Compare commits

..

No commits in common. 'master' and 'stable' have entirely different histories.

  1. 56
      robocom-zero.cabal
  2. 26
      src/ATrade/BarAggregator.hs
  3. 351
      src/ATrade/Driver/Backtest.hs
  4. 229
      src/ATrade/Driver/Junction.hs
  5. 64
      src/ATrade/Driver/Junction/BrokerService.hs
  6. 258
      src/ATrade/Driver/Junction/JunctionMonad.hs
  7. 72
      src/ATrade/Driver/Junction/ProgramConfiguration.hs
  8. 30
      src/ATrade/Driver/Junction/QuoteStream.hs
  9. 304
      src/ATrade/Driver/Junction/QuoteThread.hs
  10. 151
      src/ATrade/Driver/Junction/RemoteControl.hs
  11. 216
      src/ATrade/Driver/Junction/RobotDriverThread.hs
  12. 70
      src/ATrade/Driver/Junction/Types.hs
  13. 92
      src/ATrade/Driver/Real.hs
  14. 361
      src/ATrade/Quotes/Finam.hs
  15. 12
      src/ATrade/Quotes/HistoryProvider.hs
  16. 21
      src/ATrade/Quotes/QHP.hs
  17. 14
      src/ATrade/Quotes/QTIS.hs
  18. 12
      src/ATrade/Quotes/TickerInfoProvider.hs
  19. 0
      src/ATrade/Quotes/Types.hs
  20. 14
      src/ATrade/RoboCom/ConfigStorage.hs
  21. 32
      src/ATrade/RoboCom/Monad.hs
  22. 16
      src/ATrade/RoboCom/Persistence.hs
  23. 296
      src/ATrade/RoboCom/Positions.hs
  24. 40
      src/ATrade/RoboCom/Types.hs
  25. 7
      src/ATrade/RoboCom/Utils.hs
  26. 5
      stack.yaml
  27. 2
      test/ArbitraryInstances.hs
  28. 6
      test/Spec.hs
  29. 228
      test/Test/BarAggregator.hs
  30. 117
      test/Test/Driver/Junction/QuoteThread.hs
  31. 27
      test/Test/Mock/HistoryProvider.hs
  32. 22
      test/Test/Mock/TickerInfoProvider.hs
  33. 2
      test/Test/RoboCom/Indicators.hs
  34. 6
      test/Test/RoboCom/Positions.hs
  35. 2
      test/Test/RoboCom/Utils.hs

56
robocom-zero.cabal

@ -1,5 +1,5 @@
name: robocom-zero name: robocom-zero
version: 0.2.1.0 version: 0.2.0.0
-- synopsis: -- synopsis:
-- description: -- description:
homepage: https://github.com/asakul/robocom-zero#readme homepage: https://github.com/asakul/robocom-zero#readme
@ -17,71 +17,63 @@ library
hs-source-dirs: src hs-source-dirs: src
ghc-options: -Wall -fno-warn-orphans -Wno-type-defaults ghc-options: -Wall -fno-warn-orphans -Wno-type-defaults
exposed-modules: ATrade.RoboCom.Indicators exposed-modules: ATrade.RoboCom.Indicators
, ATrade.RoboCom.ConfigStorage
, ATrade.RoboCom.Monad , ATrade.RoboCom.Monad
, ATrade.RoboCom.Positions , ATrade.RoboCom.Positions
, ATrade.RoboCom.Persistence
, ATrade.RoboCom.Types , ATrade.RoboCom.Types
, ATrade.RoboCom.Utils , ATrade.RoboCom.Utils
, ATrade.Quotes , ATrade.Quotes
, ATrade.Quotes.Finam
, ATrade.Quotes.QHP , ATrade.Quotes.QHP
, ATrade.Quotes.QTIS , ATrade.Quotes.QTIS
, ATrade.Driver.Real
, ATrade.Driver.Backtest , ATrade.Driver.Backtest
, ATrade.Driver.Junction , ATrade.Driver.Junction
, ATrade.Driver.Junction.Types , ATrade.Driver.Junction.Types
, ATrade.Driver.Junction.QuoteThread
, ATrade.Driver.Junction.QuoteStream
, ATrade.Driver.Junction.RobotDriverThread
, ATrade.Driver.Junction.ProgramConfiguration
, ATrade.Driver.Junction.BrokerService
, ATrade.BarAggregator , ATrade.BarAggregator
, ATrade.RoboCom , ATrade.RoboCom
, ATrade.Quotes.HistoryProvider
, ATrade.Quotes.TickerInfoProvider
other-modules: Paths_robocom_zero other-modules: Paths_robocom_zero
, ATrade.Driver.Junction.RemoteControl
, ATrade.Driver.Junction.JunctionMonad
build-depends: base >= 4.7 && < 5 build-depends: base >= 4.7 && < 5
, libatrade >= 0.16.0.0 && < 0.17.0.0 , libatrade >= 0.9.0.0 && < 0.10.0.0
, text , text
, text-icu , text-icu
, errors
, lens , lens
, bytestring , bytestring
, cassava
, containers , containers
, time , time
, vector , vector
, wreq
, safe , safe
, hslogger
, parsec
, parsec-numbers
, aeson , aeson
, binary , binary
, binary-ieee754 , binary-ieee754
, zeromq4-haskell , zeromq4-haskell
, zeromq4-haskell-zap
, unordered-containers , unordered-containers
, hashable
, th-printf , th-printf
, BoundedChan , BoundedChan
, monad-loops , monad-loops
, conduit
, safe-exceptions , safe-exceptions
, mtl , mtl
, transformers , transformers
, list-extras
, optparse-applicative , optparse-applicative
, split
, signal , signal
, random
, hedis , hedis
, gitrev , gitrev
, data-default , data-default
, template-haskell , template-haskell
, bimap
, dhall
, extra
, co-log
, text-show
, unliftio
, conduit
, split
, cassava
default-language: Haskell2010 default-language: Haskell2010
other-modules: ATrade.Exceptions other-modules: ATrade.Exceptions
, ATrade.Driver.Real.BrokerClientThread
, ATrade.Driver.Real.QuoteSourceThread
, ATrade.Driver.Types , ATrade.Driver.Types
test-suite robots-test test-suite robots-test
@ -93,33 +85,23 @@ test-suite robots-test
, libatrade , libatrade
, time , time
, text , text
, hedgehog
, tasty , tasty
, tasty-hunit , tasty-hunit
, tasty-golden , tasty-golden
, tasty-hedgehog
, tasty-hspec
, tasty-quickcheck
, tasty-smallcheck , tasty-smallcheck
, tasty-quickcheck
, tasty-hspec
, quickcheck-text , quickcheck-text
, quickcheck-instances , quickcheck-instances
, containers , containers
, safe , safe
, zeromq4-haskell
, zeromq4-haskell-zap
, BoundedChan
, mtl
, co-log-core
, co-log
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010 default-language: Haskell2010
other-modules: Test.RoboCom.Indicators other-modules: Test.RoboCom.Indicators
, Test.RoboCom.Positions
, Test.RoboCom.Utils , Test.RoboCom.Utils
, Test.Driver.Junction.QuoteThread
, Test.BarAggregator , Test.BarAggregator
, ArbitraryInstances , ArbitraryInstances
, Test.Mock.HistoryProvider
, Test.Mock.TickerInfoProvider
source-repository head source-repository head
type: git type: git

26
src/ATrade/BarAggregator.hs

@ -72,32 +72,29 @@ handleTicks ticks aggregator = foldl f ([], aggregator) ticks
handleTick :: Tick -> BarAggregator -> (Maybe Bar, BarAggregator) handleTick :: Tick -> BarAggregator -> (Maybe Bar, BarAggregator)
handleTick tick = runState $ do handleTick tick = runState $ do
lLastTicks %= M.insert (security tick, datatype tick) tick lLastTicks %= M.insert (security tick, datatype tick) tick
timeWindows <- gets tickTimeWindows tws <- gets tickTimeWindows
mybars <- gets bars mybars <- gets bars
if any (isInTimeInterval tick) timeWindows if (any (isInTimeInterval tick) tws)
then then
case M.lookup (security tick) mybars of case M.lookup (security tick) mybars of
Just series -> case bsBars series of Just series -> case bsBars series of
(b:bs) -> do (b:bs) -> do
let timeframeInSeconds = fromIntegral . unBarTimeframe $ bsTimeframe series let currentBn = barNumber (barTimestamp b) (tfSeconds $ bsTimeframe series)
let currentBn = barNumber (barTimestamp b) timeframeInSeconds
case datatype tick of case datatype tick of
LastTradePrice -> LastTradePrice ->
if volume tick > 0 if volume tick > 0
then then
if currentBn == barNumber (timestamp tick) timeframeInSeconds if currentBn == barNumber (timestamp tick) (tfSeconds $ bsTimeframe series)
then do then do
lBars %= M.insert (security tick) series { bsBars = updateBar b tick : bs } lBars %= M.insert (security tick) series { bsBars = updateBar b tick : bs }
return Nothing return Nothing
else do else do
let barEndTimestamp = barEndTime b timeframeInSeconds lBars %= M.insert (security tick) series { bsBars = barFromTick tick : b : bs }
let resultingBar = b { barTimestamp = barEndTimestamp } return . Just $ b
lBars %= M.insert (security tick) series { bsBars = barFromTick tick : resultingBar : bs }
return . Just $ resultingBar
else else
return Nothing return Nothing
_ -> _ ->
if currentBn == barNumber (timestamp tick) (fromIntegral . unBarTimeframe $ bsTimeframe series) if currentBn == barNumber (timestamp tick) (tfSeconds $ bsTimeframe series)
then do then do
lBars %= M.insert (security tick) series { bsBars = updateBarTimestamp b tick : bs } lBars %= M.insert (security tick) series { bsBars = updateBarTimestamp b tick : bs }
return Nothing return Nothing
@ -143,16 +140,15 @@ handleTick tick = runState $ do
updateTime :: Tick -> BarAggregator -> (Maybe Bar, BarAggregator) updateTime :: Tick -> BarAggregator -> (Maybe Bar, BarAggregator)
updateTime tick = runState $ do updateTime tick = runState $ do
lLastTicks %= M.insert (security tick, datatype tick) tick lLastTicks %= M.insert (security tick, datatype tick) tick
timeWindows <- gets tickTimeWindows tws <- gets tickTimeWindows
mybars <- gets bars mybars <- gets bars
if any (isInTimeInterval tick) timeWindows if (any (isInTimeInterval tick) tws)
then then
case M.lookup (security tick) mybars of case M.lookup (security tick) mybars of
Just series -> case bsBars series of Just series -> case bsBars series of
(b:bs) -> do (b:bs) -> do
let timeframeInSeconds = fromIntegral . unBarTimeframe $ bsTimeframe series let currentBn = barNumber (barTimestamp b) (tfSeconds $ bsTimeframe series)
let currentBn = barNumber (barTimestamp b) timeframeInSeconds let thisBn = barNumber (timestamp tick) (tfSeconds $ bsTimeframe series)
let thisBn = barNumber (timestamp tick) timeframeInSeconds
if if
| currentBn == thisBn -> do | currentBn == thisBn -> do
lBars %= M.insert (security tick) series { bsBars = updateBarTimestamp b tick : bs } lBars %= M.insert (security tick) series { bsBars = updateBarTimestamp b tick : bs }

351
src/ATrade/Driver/Backtest.hs

@ -13,101 +13,59 @@ module ATrade.Driver.Backtest (
backtestMain backtestMain
) where ) where
import ATrade.Driver.Junction.Types (StrategyDescriptor (StrategyDescriptor), import ATrade.Driver.Types (InitializationCallback,
StrategyDescriptorE (StrategyDescriptorE), StrategyInstanceParams (..))
TickerConfig, confStrategy, import ATrade.Exceptions
confTickers, eventCallback, import ATrade.Quotes
strategyBaseName, tickerId, import ATrade.Quotes.Finam as QF
timeframe) import ATrade.Quotes.QTIS
import ATrade.Exceptions (RoboComException (UnableToLoadConfig, UnableToLoadFeed)) import ATrade.RoboCom.Monad (Event (..), EventCallback,
import ATrade.Logging (Message, Severity (Error, Trace), MonadRobot (..),
fmtMessage, logWith)
import ATrade.Quotes.QTIS (TickerInfo (tiLotSize, tiTickSize),
qtisGetTickersInfo)
import ATrade.RoboCom.ConfigStorage (ConfigStorage (loadConfig))
import ATrade.RoboCom.Monad (Event (..), MonadRobot (..),
StrategyEnvironment (..), StrategyEnvironment (..),
appendToLog, seLastTimestamp) appendToLog, seBars, seLastTimestamp)
import ATrade.RoboCom.Types (BarSeries (..), import ATrade.RoboCom.Positions
BarSeriesId (BarSeriesId), Bars, import ATrade.RoboCom.Types (BarSeries (..), Bars, InstrumentParameters (InstrumentParameters),
InstrumentParameters (InstrumentParameters), Ticker (..), Timeframe (..))
Ticker (..)) import ATrade.Types
import ATrade.Types (Bar (Bar, barHigh, barLow, barOpen, barSecurity, barTimestamp), import Conduit (awaitForever, runConduit, yield,
BarTimeframe (BarTimeframe), (.|))
Operation (Buy), import Control.Exception.Safe
Order (orderAccountId, orderId, orderOperation, orderPrice, orderQuantity, orderSecurity, orderSignalId), import Control.Lens hiding (ix, (<|), (|>))
OrderId,
OrderPrice (Limit, Market),
OrderState (Cancelled, Executed, Submitted),
Price, TickerId, Trade (..),
fromDouble)
import Colog (LogAction, (>$<))
import Colog.Actions (logTextStdout)
import Conduit (ConduitT, Void, awaitForever,
runConduit, yield, (.|))
import Control.Exception.Safe (catchAny, throw)
import Control.Lens (makeLenses, use, (%=), (+=),
(.=), (^.))
import Control.Monad.ST (runST) import Control.Monad.ST (runST)
import Control.Monad.State (MonadIO, MonadPlus (mzero), import Control.Monad.State
MonadState, MonadTrans (lift), import Data.Aeson (FromJSON (..), Value (..), decode)
State, StateT (StateT),
execState, forM_, gets, when)
import Data.Aeson (FromJSON (..), Value (..),
decode)
import Data.Aeson.Types (parseMaybe) import Data.Aeson.Types (parseMaybe)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Data.ByteString.Lazy (readFile, toStrict) import Data.ByteString.Lazy (readFile, toStrict)
import qualified Data.ByteString.Lazy as BL import Data.Default
import Data.Csv (FromField (parseField),
FromRecord (parseRecord),
HasHeader (HasHeader), (.!))
import qualified Data.Csv as Csv
import Data.Default (Default (def))
import Data.HashMap.Strict (lookup) import Data.HashMap.Strict (lookup)
import Data.IORef (newIORef)
import Data.List (partition) import Data.List (partition)
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.List.Split (splitOn) import Data.List.Split (splitOn)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Sequence (Seq (..), (<|), (|>)) import Data.Sequence (Seq (..), (<|), (|>))
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import Data.STRef (newSTRef, readSTRef, writeSTRef) import Data.STRef (newSTRef, readSTRef, writeSTRef)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Text.IO (putStrLn) import Data.Text.IO (putStrLn)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Data.Time (defaultTimeLocale, parseTimeM)
import Data.Time.Calendar (fromGregorian) import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..), addUTCTime) import Data.Time.Clock (DiffTime, UTCTime (..))
import Data.Vector ((!), (!?), (//)) import Data.Vector ((!), (!?), (//))
import qualified Data.Vector as V import qualified Data.Vector as V
import Dhall (FromDhall, auto, input) import Options.Applicative hiding (Success)
import Options.Applicative (Alternative (some), Parser, import Prelude hiding (lookup, putStrLn, readFile)
ReadM, eitherReader, execParser,
fullDesc, header, helper, info,
long, metavar, option, short,
strOption)
import Prelude hiding (log, lookup, putStrLn,
readFile)
import Safe (headMay) import Safe (headMay)
import System.IO (IOMode (ReadMode), withFile) import System.ZMQ4 hiding (Event)
import System.ZMQ4 (withContext)
data Feed = Feed TickerId FilePath data Feed = Feed TickerId FilePath
deriving (Show, Eq) deriving (Show, Eq)
data Params = Params { data Params = Params {
strategyBasename :: String,
strategyConfigFile :: FilePath, strategyConfigFile :: FilePath,
qtisEndpoint :: String, qtisEndpoint :: String,
paramsFeeds :: [Feed] paramsFeeds :: [Feed]
} deriving (Show, Eq) } deriving (Show, Eq)
data BacktestState c s = BacktestState { data BacktestState c s = BacktestState {
_descriptor :: StrategyDescriptor c s,
_cash :: Double, _cash :: Double,
_robotState :: s, _robotState :: s,
_robotParams :: c, _robotParams :: c,
@ -117,135 +75,101 @@ data BacktestState c s = BacktestState {
_tradesLog :: [Trade], _tradesLog :: [Trade],
_orderIdCounter :: Integer, _orderIdCounter :: Integer,
_pendingTimers :: [UTCTime], _pendingTimers :: [UTCTime],
_logs :: [T.Text], _logs :: [T.Text]
_barsMap :: M.Map BarSeriesId BarSeries,
_availableTickers :: NonEmpty BarSeriesId
} }
makeLenses ''BacktestState makeLenses ''BacktestState
data Row = Row {
rowTicker :: T.Text,
rowTimeframe :: Int,
rowTime :: UTCTime,
rowOpen :: Price,
rowHigh :: Price,
rowLow :: Price,
rowClose :: Price,
rowVolume :: Integer
} deriving (Show, Eq)
instance FromField Price where
parseField s = fromDouble <$> (parseField s :: Csv.Parser Double)
instance FromRecord Row where
parseRecord v
| length v == 9 = do
tkr <- v .! 0
tf <- v .! 1
date <- v .! 2
time <- v .! 3
dt <- addUTCTime (-3 * 3600) <$> parseDt date time
open <- v .! 4
high <- v .! 5
low <- v .! 6
close <- v .! 7
vol <- v .! 8
return $ Row tkr tf dt open high low close vol
| otherwise = mzero
where
parseDt :: B.ByteString -> B.ByteString -> Csv.Parser UTCTime
parseDt d t = case parseTimeM True defaultTimeLocale "%Y%m%d %H%M%S" $ B8.unpack d ++ " " ++ B8.unpack t of
Just dt -> return dt
Nothing -> fail "Unable to parse date/time"
parseQuotes :: B.ByteString -> Maybe [Row]
parseQuotes csvData = case Csv.decode HasHeader $ BL.fromStrict csvData of
Left _ -> Nothing
Right d -> Just $ V.toList d
paramsParser :: Parser Params paramsParser :: Parser Params
paramsParser = Params paramsParser = Params
<$> strOption ( <$> strOption (
long "strategy-name" <> short 'n') long "config" <> short 'c'
<*> strOption ( )
long "config" <> short 'c')
<*> strOption <*> strOption
( long "qtis" <> short 'q' <> metavar "ENDPOINT/ID") ( long "qtis" <> short 'q' <> metavar "ENDPOINT/ID" )
<*> some (option feedArgParser ( <*> some (option feedArgParser (
long "feed" <> short 'f')) long "feed" <> short 'f'
))
feedArgParser :: ReadM Feed feedArgParser :: ReadM Feed
feedArgParser = eitherReader (\s -> case splitOn ":" s of feedArgParser = eitherReader (\s -> case splitOn ":" s of
[tid, fpath] -> Right $ Feed (T.pack tid) fpath [tid, fpath] -> Right $ Feed (T.pack tid) fpath
_ -> Left $ "Unable to parse feed id: " ++ s) _ -> Left $ "Unable to parse feed id: " ++ s)
logger :: (MonadIO m) => LogAction m Message backtestMain :: (FromJSON c, StateHasPositions s) => DiffTime -> s -> EventCallback c s -> IO ()
logger = fmtMessage >$< logTextStdout backtestMain _dataDownloadDelta defaultState callback = do
backtestMain :: M.Map T.Text StrategyDescriptorE -> IO ()
backtestMain descriptors = do
params <- execParser opts params <- execParser opts
let log = logWith logger (tickerList, config) <- loadStrategyConfig params
let strategyName = T.pack $ strategyBasename params
let instanceParams = StrategyInstanceParams {
strategyInstanceId = "foo",
strategyAccount = "foo",
strategyVolume = 1,
tickers = tickerList,
strategyQTISEp = Nothing }
feeds <- loadFeeds (paramsFeeds params) feeds <- loadFeeds (paramsFeeds params)
case M.lookup strategyName descriptors of bars <- makeBars (T.pack $ qtisEndpoint params) tickerList
Just (StrategyDescriptorE desc) -> flip catchAny (\e -> log Error "Backtest" $ "Exception: " <> (T.pack . show $ e)) $
runBacktestDriver desc feeds params runBacktestDriver feeds config bars
Nothing -> log Error "Backtest" $ "Can't find strategy: " <> strategyName
where where
opts = info (helper <*> paramsParser) opts = info (helper <*> paramsParser)
( fullDesc <> header "ATrade strategy backtesting framework" ) ( fullDesc <> header "ATrade strategy backtesting framework" )
makeBars :: T.Text -> [TickerConfig] -> IO (M.Map BarSeriesId BarSeries) makeBars :: T.Text -> [Ticker] -> IO (M.Map TickerId BarSeries)
makeBars qtisEp confs = makeBars qtisEp tickersList =
withContext $ \ctx -> withContext $ \ctx ->
M.fromList <$> mapM (mkBarEntry ctx qtisEp) confs M.fromList <$> mapM (mkBarEntry ctx qtisEp) tickersList
mkBarEntry ctx qtisEp conf = do mkBarEntry ctx qtisEp tickerEntry = do
info <- qtisGetTickersInfo ctx qtisEp (tickerId conf) info <- qtisGetTickersInfo ctx qtisEp (code tickerEntry)
return (BarSeriesId (tickerId conf) (timeframe conf), return (code tickerEntry, BarSeries (code tickerEntry) (Timeframe (timeframeSeconds tickerEntry)) [] (InstrumentParameters (fromInteger $ tiLotSize info) (tiTickSize info)))
BarSeries
(tickerId conf)
(timeframe conf)
[] runBacktestDriver feeds params tickerList = do
(InstrumentParameters (tickerId conf) (fromInteger $ tiLotSize info) (tiTickSize info))) let s = runConduit $ barStreamFromFeeds feeds .| backtestLoop
let finalState = execState (unBacktestingMonad s) $ defaultBacktestState defaultState params tickerList
runBacktestDriver desc feeds params = do
bigConf <- loadConfig (T.pack $ strategyConfigFile params)
case confTickers bigConf of
tickerList@(firstTicker:restTickers) -> do
bars <- makeBars (T.pack $ qtisEndpoint params) tickerList
let s = runConduit $ barStreamFromFeeds feeds .| backtestLoop desc
let finalState =
execState (unBacktestingMonad s) $ defaultBacktestState def (confStrategy bigConf) desc bars (fmap toBarSeriesId (firstTicker :| restTickers))
print $ finalState ^. cash print $ finalState ^. cash
print $ finalState ^. tradesLog print $ finalState ^. tradesLog
forM_ (reverse $ finalState ^. logs) putStrLn forM_ (reverse $ finalState ^. logs) putStrLn
_ -> return ()
toBarSeriesId conf = BarSeriesId (tickerId conf) (timeframe conf) loadStrategyConfig :: (FromJSON c) => Params -> IO ([Ticker], c)
loadStrategyConfig params = do
content <- readFile (strategyConfigFile params)
case loadStrategyConfig' content of
Just (tickersList, config) -> return (tickersList, config)
_ -> throw $ UnableToLoadConfig (T.pack . strategyConfigFile $ params)
loadStrategyConfig' content = do
v <- decode content
case v of
Object o -> do
mbTickers <- "tickers" `lookup` o
mbParams <- "params" `lookup` o
tickers' <- parseMaybe parseJSON mbTickers
params <- parseMaybe parseJSON mbParams
return (tickers', params)
_ -> Nothing
barStreamFromFeeds :: (Monad m) => V.Vector (BarTimeframe, [Bar]) -> ConduitT () (BarSeriesId, Bar) m ()
barStreamFromFeeds feeds = case nextBar feeds of barStreamFromFeeds feeds = case nextBar feeds of
Just (tf, bar, feeds') -> yield (BarSeriesId (barSecurity bar) tf, bar) >> barStreamFromFeeds feeds' Just (bar, feeds') -> yield bar >> barStreamFromFeeds feeds'
_ -> return () _ -> return ()
nextBar :: V.Vector (BarTimeframe, [Bar]) -> Maybe (BarTimeframe, Bar, V.Vector (BarTimeframe, [Bar])) nextBar :: V.Vector [Bar] -> Maybe (Bar, V.Vector [Bar])
nextBar feeds = case indexOfNextFeed feeds of nextBar feeds = case indexOfNextFeed feeds of
Just ix -> do Just ix -> do
(tf, f) <- feeds !? ix f <- feeds !? ix
h <- headMay f h <- headMay f
return (tf, h, feeds // [(ix, (tf, tail f))]) return (h, feeds // [(ix, tail f)])
_ -> Nothing _ -> Nothing
indexOfNextFeed feeds = runST $ do indexOfNextFeed feeds = runST $ do
minTs <- newSTRef Nothing minTs <- newSTRef Nothing
minIx <- newSTRef Nothing minIx <- newSTRef Nothing
forM_ [0..(V.length feeds-1)] (\ix -> do forM_ [0..(V.length feeds-1)] (\ix -> do
let (_, feed) = feeds ! ix let feed = feeds ! ix
curTs <- readSTRef minTs curTs <- readSTRef minTs
case feed of case feed of
x:_ -> case curTs of x:_ -> case curTs of
@ -258,35 +182,29 @@ backtestMain descriptors = do
_ -> return ()) _ -> return ())
readSTRef minIx readSTRef minIx
backtestLoop :: StrategyDescriptor c s -> ConduitT (BarSeriesId, Bar) Void (BacktestingMonad c s) () backtestLoop = awaitForever (\bar -> do
backtestLoop desc =
awaitForever (\(bsId, bar) -> do
_curState <- use robotState _curState <- use robotState
_env <- gets _strategyEnvironment _env <- gets _strategyEnvironment
let newTimestamp = barTimestamp bar let newTimestamp = barTimestamp bar
barsMap %= updateBars bsId bar strategyEnvironment . seBars %= (flip updateBars bar)
strategyEnvironment . seLastTimestamp .= newTimestamp strategyEnvironment . seLastTimestamp .= newTimestamp
enqueueEvent (NewBar (bsIdTf bsId, bar)) enqueueEvent (NewBar bar)
lift (handleEvents desc)) lift handleEvents)
bsIdTf (BarSeriesId _ tf) = tf
handleEvents = do
handleEvents :: StrategyDescriptor c s -> BacktestingMonad c s ()
handleEvents desc = do
events <- use pendingEvents events <- use pendingEvents
case events of case events of
x :<| xs -> do x :<| xs -> do
pendingEvents .= xs pendingEvents .= xs
handleEvent desc x handleEvent x
handleEvents desc handleEvents
_ -> return () _ -> return ()
executePendingOrders bar = do executePendingOrders bar = do
executeMarketOrders bar executeMarketOrders bar
executeLimitOrders bar executeLimitOrders bar
executeLimitOrders bar = do executeLimitOrders bar = do
(limitOrders, otherOrders'') <- partition (limitOrders, otherOrders'') <- partition
(\o -> case orderPrice o of (\o -> case orderPrice o of
Limit _ -> True Limit _ -> True
@ -295,13 +213,13 @@ executeLimitOrders bar = do
pendingOrders .= otherOrders' ++ otherOrders'' pendingOrders .= otherOrders' ++ otherOrders''
forM_ executableOrders $ \order -> order `executeAtPrice` priceForLimitOrder order bar forM_ executableOrders $ \order -> order `executeAtPrice` priceForLimitOrder order bar
isExecutable bar order = case orderPrice order of isExecutable bar order = case orderPrice order of
Limit price -> if orderOperation order == Buy Limit price -> if orderOperation order == Buy
then price >= barLow bar then price >= barLow bar
else price <= barHigh bar else price <= barHigh bar
_ -> True _ -> True
priceForLimitOrder order bar = case orderPrice order of priceForLimitOrder order bar = case orderPrice order of
Limit price -> if orderOperation order == Buy Limit price -> if orderOperation order == Buy
then if price >= barOpen bar then if price >= barOpen bar
then barOpen bar then barOpen bar
@ -311,21 +229,21 @@ priceForLimitOrder order bar = case orderPrice order of
else price else price
_ -> error "Should've been limit order" _ -> error "Should've been limit order"
executeMarketOrders bar = do executeMarketOrders bar = do
(marketOrders, otherOrders) <- partition (\o -> orderPrice o == Market) <$> use pendingOrders (marketOrders, otherOrders) <- partition (\o -> orderPrice o == Market) <$> use pendingOrders
pendingOrders .= otherOrders pendingOrders .= otherOrders
forM_ marketOrders $ \order -> forM_ marketOrders $ \order ->
order `executeAtPrice` barOpen bar order `executeAtPrice` barOpen bar
executeAtPrice order price = do executeAtPrice order price = do
ts <- use $ strategyEnvironment . seLastTimestamp ts <- use $ strategyEnvironment . seLastTimestamp
let thisTrade = mkTrade order price ts let thisTrade = mkTrade order price ts
tradesLog %= (thisTrade :) tradesLog %= (\log' -> thisTrade : log')
pendingEvents %= (\s -> OrderUpdate (orderId order) Executed <| s) pendingEvents %= (\s -> (OrderUpdate (orderId order) Executed) <| s)
pendingEvents %= (\s -> NewTrade thisTrade <| s) pendingEvents %= (\s -> (NewTrade thisTrade) <| s)
mkTrade :: Order -> Price -> UTCTime -> Trade mkTrade :: Order -> Price -> UTCTime -> Trade
mkTrade order price ts = Trade { mkTrade order price ts = Trade {
tradeOrderId = orderId order, tradeOrderId = orderId order,
tradePrice = price, tradePrice = price,
tradeQuantity = orderQuantity order, tradeQuantity = orderQuantity order,
@ -339,43 +257,51 @@ mkTrade order price ts = Trade {
tradeSignalId = orderSignalId order tradeSignalId = orderSignalId order
} }
handleEvent :: StrategyDescriptor c s -> Event -> BacktestingMonad c s () handleEvent event@(NewBar bar) = do
handleEvent desc event@(NewBar (_, bar)) = do
executePendingOrders bar executePendingOrders bar
handleEvents desc -- This should pass OrderUpdate events to the callback before NewBar events handleEvents -- This should pass OrderUpdate events to the callback before NewBar events
firedTimers <- fireTimers (barTimestamp bar) firedTimers <- fireTimers (barTimestamp bar)
mapM_ (enqueueEvent . TimerFired) firedTimers mapM_ (\x -> enqueueEvent (TimerFired x)) firedTimers
handleEvent' desc event handleEvent' event
return () return ()
handleEvent desc event = handleEvent' desc event handleEvent event = handleEvent' event
handleEvent' event = callback event
handleEvent' desc event = eventCallback desc event updateBars barMap newbar = M.alter (\case
Nothing -> Just BarSeries { bsTickerId = barSecurity newbar,
bsTimeframe = Timeframe 60,
bsBars = [newbar, newbar] }
Just bs -> Just bs { bsBars = updateBarList newbar (bsBars bs) }) (barSecurity newbar) barMap
updateBars bsId newbar barMap = M.adjust (\bs -> bs { bsBars = newbar : bsBars bs }) bsId barMap updateBarList newbar (_:bs) = newbar:newbar:bs
updateBarList newbar _ = newbar:[newbar]
fireTimers ts = do fireTimers ts = do
(firedTimers, otherTimers) <- partition (< ts) <$> use pendingTimers (firedTimers, otherTimers) <- partition (< ts) <$> use pendingTimers
pendingTimers .= otherTimers pendingTimers .= otherTimers
return firedTimers return firedTimers
loadFeeds :: [Feed] -> IO (V.Vector (BarTimeframe, [Bar])) loadFeeds :: [Feed] -> IO (V.Vector [Bar])
loadFeeds feeds = V.fromList <$> mapM loadFeed feeds loadFeeds feeds = V.fromList <$> mapM loadFeed feeds
loadFeed (Feed tid path) = do loadFeed (Feed tid path) = do
content <- readFile path content <- readFile path
case parseQuotes $ toStrict content of case QF.parseQuotes $ toStrict content of
Just quotes -> case headMay quotes of Just quotes -> return $ fmap (rowToBar tid) quotes
Just first -> return (BarTimeframe (rowTimeframe first), fmap (rowToBar tid) quotes)
Nothing -> throw $ UnableToLoadFeed (T.pack path)
_ -> throw $ UnableToLoadFeed (T.pack path) _ -> throw $ UnableToLoadFeed (T.pack path)
rowToBar tid r = Bar tid (rowTime r) (rowOpen r) (rowHigh r) (rowLow r) (rowClose r) (rowVolume r) rowToBar tid r = Bar tid (rowTime r) (rowOpen r) (rowHigh r) (rowLow r) (rowClose r) (rowVolume r)
enqueueEvent :: MonadState (BacktestState c s) m => Event -> m () enqueueEvent event = pendingEvents %= (\s -> s |> event)
enqueueEvent event = pendingEvents %= (|> event)
instance (Default c, Default s) => Default (BacktestState c s)
where
def = defaultBacktestState def def def
defaultBacktestState :: s -> c -> StrategyDescriptor c s -> M.Map BarSeriesId BarSeries -> NonEmpty BarSeriesId -> BacktestState c s defaultBacktestState :: s -> c -> Bars -> BacktestState c s
defaultBacktestState s c desc = BacktestState desc 0 s c (StrategyEnvironment "" "" 1 (UTCTime (fromGregorian 1970 1 1) 0)) [] Seq.empty [] 1 [] [] defaultBacktestState s c bars = BacktestState 0 s c (StrategyEnvironment "" "" 1 bars (UTCTime (fromGregorian 1970 1 1) 0)) [] Seq.empty [] 1 [] []
newtype BacktestingMonad s c a = BacktestingMonad { unBacktestingMonad :: State (BacktestState s c) a } newtype BacktestingMonad s c a = BacktestingMonad { unBacktestingMonad :: State (BacktestState s c) a }
deriving (Functor, Applicative, Monad, MonadState (BacktestState s c)) deriving (Functor, Applicative, Monad, MonadState (BacktestState s c))
@ -389,38 +315,21 @@ instance MonadRobot (BacktestingMonad c s) c s where
submitOrder order = do submitOrder order = do
oid <- nextOrderId oid <- nextOrderId
let orderWithId = order { orderId = oid } let orderWithId = order { orderId = oid }
pendingOrders %= (orderWithId :) pendingOrders %= ((:) orderWithId)
pendingEvents %= (\s -> s |> OrderUpdate oid Submitted) pendingEvents %= (\s -> s |> (OrderSubmitted orderWithId))
return oid
cancelOrder oid = do cancelOrder oid = do
orders <- use pendingOrders orders <- use pendingOrders
let (matchingOrders, otherOrders) = partition (\o -> orderId o == oid) orders let (matchingOrders, otherOrders) = partition (\o -> orderId o == oid) orders
case matchingOrders of case matchingOrders of
[] -> return () [] -> return ()
xs -> do xs -> do
mapM_ (\o -> pendingEvents %= (\s -> s |> OrderUpdate (orderId o) Cancelled)) xs mapM_ (\o -> pendingEvents %= (\s -> s |> (OrderUpdate (orderId o) Cancelled))) xs
pendingOrders .= otherOrders pendingOrders .= otherOrders
appendToLog _ txt = logs %= ((TL.toStrict txt) :) appendToLog txt = logs %= ((:) (TL.toStrict txt))
setupTimer time = pendingTimers %= (time :) setupTimer time = pendingTimers %= ((:) time)
enqueueIOAction _actionId _action = error "Backtesting io actions is not supported" enqueueIOAction _actionId _action = error "Backtesting io actions is not supported"
getConfig = use robotParams getConfig = use robotParams
getState = use robotState getState = use robotState
setState s = robotState .= s setState s = robotState .= s
getEnvironment = use strategyEnvironment getEnvironment = use strategyEnvironment
getTicker tid tf = do
m <- gets _barsMap
return $ M.lookup (BarSeriesId tid tf) m
getTickerInfo tid = do
tickers <- getAvailableTickers
case L.find (\(BarSeriesId t _) -> t == tid) tickers of
Just (BarSeriesId t tf) -> do
ticker <- getTicker t tf
return (bsParams <$> ticker)
Nothing -> return Nothing
getAvailableTickers = use availableTickers
instance ConfigStorage IO where
loadConfig filepath = do
cfg <- B.readFile $ T.unpack filepath
input auto (decodeUtf8 cfg)

229
src/ATrade/Driver/Junction.hs

@ -1,211 +1,58 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Junction module ATrade.Driver.Junction
( (
junctionMain junctionMain
) where ) where
import ATrade.Broker.Client (startBrokerClient, import ATrade.Driver.Junction.Types (StrategyDescriptor (..),
stopBrokerClient) StrategyInstance (..),
import ATrade.Broker.Protocol (Notification (OrderNotification, TradeNotification), StrategyInstanceDescriptor (..))
NotificationSqnum (unNotificationSqnum), import Data.Aeson (decode)
getNotificationSqnum) import qualified Data.ByteString as B
import ATrade.Driver.Junction.BrokerService (getNotifications, import qualified Data.ByteString.Lazy as BL
mkBrokerService) import Data.IORef
import ATrade.Driver.Junction.JunctionMonad (JunctionEnv (..),
JunctionM (..),
saveRobots,
startRobot)
import ATrade.Driver.Junction.ProgramConfiguration (ProgramConfiguration (..),
ProgramOptions (ProgramOptions, configPath))
import ATrade.Driver.Junction.QuoteThread (DownloaderEnv (DownloaderEnv),
withQThread)
import ATrade.Driver.Junction.RemoteControl (handleRemoteControl)
import ATrade.Driver.Junction.RobotDriverThread (RobotDriverHandle, postNotificationEvent)
import ATrade.Driver.Junction.Types (StrategyDescriptorE)
import ATrade.Logging (Message (..), Severity (Debug, Info, Trace, Warning),
fmtMessage,
logWith)
import ATrade.Quotes.QHP (mkQHPHandle)
import ATrade.Types (OrderId, Trade (tradeOrderId))
import Colog (LogAction (LogAction),
cfilter,
hoistLogAction,
logTextStderr,
(<&), (>$<))
import Colog.Actions (logTextHandle)
import Control.Concurrent.QSem (newQSem)
import Control.Monad (forM_, forever)
import Control.Monad.Extra (whenM)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Reader (ReaderT (runReaderT))
import Data.IORef (IORef,
atomicModifyIORef',
newIORef,
readIORef)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Set (notMember)
import qualified Data.Set as S
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.IO (readFile)
import Database.Redis (ConnectInfo (..), PortID (UnixSocket),
checkedConnect,
defaultConnectInfo)
import Dhall (auto, input)
import Options.Applicative (Parser,
execParser,
fullDesc, header,
help, helper,
info, long,
metavar, progDesc,
short, strOption,
(<**>))
import Prelude hiding (log,
readFile)
import System.IO (BufferMode (LineBuffering),
Handle,
IOMode (AppendMode),
hSetBuffering,
withFile)
import System.ZMQ4 (Router (Router),
bind, withContext,
withSocket)
import UnliftIO (MonadUnliftIO)
import UnliftIO.Exception (bracket)
import UnliftIO.QSem (QSem, withQSem)
load :: T.Text -> IO B.ByteString
load = undefined
locked :: (MonadIO m, MonadUnliftIO m) => QSem -> LogAction m a -> LogAction m a junctionMain :: M.Map T.Text StrategyDescriptor -> IO ()
locked sem action = LogAction (\m -> withQSem sem (action <& m))
logger :: (MonadIO m) => M.Map T.Text Severity -> Handle -> LogAction m Message
logger loglevels h = cfilter checkLoglevel (fmtMessage >$< (logTextStderr <> logTextHandle h))
where
checkLoglevel msg =
case M.lookup (msgComponent msg) loglevels of
Just level -> msgSeverity msg >= level
Nothing -> True
junctionMain :: M.Map T.Text StrategyDescriptorE -> IO ()
junctionMain descriptors = do junctionMain descriptors = do
opts <- parseOptions parseOptions
instanceDescriptors <- undefined
let initialLogger = fmtMessage >$< logTextStderr strategies <- mkStrategies instanceDescriptors
logWith initialLogger Info "Junction" $ "Reading config from: " <> (T.pack . show) (configPath opts)
cfg <- readFile (configPath opts) >>= input auto
withFile (logBasePath cfg <> "/all.log") AppendMode $ \h -> do
hSetBuffering h LineBuffering
locksem <- newQSem 1 start strategies
let globalLogger = locked locksem (logger (M.fromList $ logLevels cfg) h)
let log = logWith globalLogger
barsMap <- newIORef M.empty where
tickerInfoMap <- newIORef M.empty parseOptions = undefined
log Info "Junction" $ "Connecting to redis: " <> redisSocket cfg mkStrategies :: [StrategyInstanceDescriptor] -> IO [StrategyInstance]
redis <- checkedConnect (defaultConnectInfo { connectPort = UnixSocket (T.unpack $ redisSocket cfg) }) mkStrategies = mapM mkStrategy
log Info "Junction" "redis: connected"
withContext $ \ctx -> do mkStrategy :: StrategyInstanceDescriptor -> IO StrategyInstance
log Debug "Junction" "0mq context created" mkStrategy desc = do
let downloaderEnv = DownloaderEnv (mkQHPHandle ctx (qhpEndpoint cfg)) ctx (qtisEndpoint cfg) (hoistLogAction liftIO globalLogger) sState <- load (stateKey desc)
robotsMap <- newIORef M.empty sCfg <- load (configKey desc)
ordersMap <- newIORef M.empty case M.lookup (strategyId desc) descriptors of
handledNotifications <- newIORef S.empty Just (StrategyDescriptor _sName sCallback _sDefState) ->
withBroker cfg robotsMap ordersMap handledNotifications globalLogger $ \bro -> case (decode $ BL.fromStrict sCfg, decode $ BL.fromStrict sState) of
withQThread downloaderEnv barsMap tickerInfoMap cfg ctx globalLogger $ \qt -> (Just pCfg, Just pState) -> do
withSocket ctx Router $ \rcSocket -> do cfgRef <- newIORef pCfg
liftIO $ bind rcSocket (T.unpack . remoteControlEndpoint $ cfg) stateRef <- newIORef pState
broService <- mkBrokerService bro ordersMap return $ StrategyInstance
let junctionLogAction = hoistLogAction liftIO globalLogger
let env =
JunctionEnv
{ {
peRedisSocket = redis, strategyInstanceId = strategyName desc,
peConfigPath = robotsConfigsPath cfg, strategyEventCallback = sCallback,
peQuoteThread = qt, strategyState = stateRef,
peBroker = bro, strategyConfig = cfgRef
peRobots = robotsMap,
peRemoteControlSocket = rcSocket,
peLogAction = junctionLogAction,
peIoLogAction = globalLogger,
peProgramConfiguration = cfg,
peBarsMap = barsMap,
peTickerInfoMap = tickerInfoMap,
peBrokerService = broService,
peDescriptors = descriptors
} }
withJunction env $ do _ -> undefined
startRobots cfg _ -> undefined
forever $ do
notifications <- getNotifications broService
forM_ notifications (liftIO . handleBrokerNotification robotsMap ordersMap handledNotifications globalLogger)
saveRobots
handleRemoteControl 1000
where
startRobots :: ProgramConfiguration -> JunctionM ()
startRobots cfg = forM_ (instances cfg) startRobot
withJunction :: JunctionEnv -> JunctionM () -> IO ()
withJunction env = (`runReaderT` env) . unJunctionM
handleBrokerNotification :: IORef (M.Map T.Text RobotDriverHandle) ->
IORef (M.Map OrderId T.Text) ->
IORef (S.Set NotificationSqnum) ->
LogAction IO Message ->
Notification ->
IO ()
handleBrokerNotification robotsRef ordersMapRef handled logger' notification= do
logWith logger' Trace "Junction" $ "Incoming notification: " <> (T.pack . show . unNotificationSqnum . getNotificationSqnum) notification
whenM (notMember (getNotificationSqnum notification) <$> readIORef handled) $ do
robotsMap <- readIORef robotsRef
ordersMap <- readIORef ordersMapRef
case getNotificationTarget robotsMap ordersMap notification of
Just robot -> postNotificationEvent robot notification
Nothing -> do
logWith logger' Warning "Junction" $ "Unknown order: " <> (T.pack . show) (notificationOrderId notification)
logWith logger' Debug "Junction" $ "Ordermap: " <> (T.pack . show) (M.toList ordersMap)
atomicModifyIORef' handled (\s -> (S.insert (getNotificationSqnum notification) s, ()))
getNotificationTarget :: M.Map T.Text RobotDriverHandle -> M.Map OrderId T.Text -> Notification -> Maybe RobotDriverHandle
getNotificationTarget robotsMap ordersMap notification = do
robotId <- M.lookup (notificationOrderId notification) ordersMap
M.lookup robotId robotsMap
notificationOrderId (OrderNotification _ oid _) = oid start = undefined
notificationOrderId (TradeNotification _ trade) = tradeOrderId trade
withBroker cfg robotsMap ordersMap handled logger' f = do
bracket
(startBrokerClient
(brokerIdentity cfg)
(brokerEndpoint cfg)
[handleBrokerNotification robotsMap ordersMap handled logger']
logger')
stopBrokerClient f
parseOptions = execParser options
options = info (optionsParser <**> helper)
(fullDesc <>
progDesc "Robocom-zero junction mode driver" <>
header "robocom-zero-junction")
optionsParser :: Parser ProgramOptions
optionsParser = ProgramOptions
<$> strOption
(long "config" <>
short 'c' <>
metavar "FILENAME" <>
help "Configuration file path")

64
src/ATrade/Driver/Junction/BrokerService.hs

@ -1,64 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module ATrade.Driver.Junction.BrokerService
(
BrokerService,
mkBrokerService,
submitOrder,
cancelOrder,
getNotifications
) where
import qualified ATrade.Broker.Client as Bro
import ATrade.Broker.Protocol (Notification (..))
import ATrade.Logging (Message, logDebug, logWarning)
import ATrade.Types (Order (..), OrderId)
import Colog (WithLog)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Reader.Class (MonadReader)
import Data.IORef (IORef, atomicModifyIORef',
newIORef)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
data BrokerService =
BrokerService
{
broker :: Bro.BrokerClientHandle,
orderMap :: IORef (M.Map OrderId T.Text),
orderIdCounter :: IORef OrderId
}
mkBrokerService :: Bro.BrokerClientHandle -> IORef (M.Map OrderId T.Text) -> IO BrokerService
mkBrokerService h om = BrokerService h om <$> newIORef 1
submitOrder :: (MonadIO m, WithLog env Message m, MonadReader env m) => BrokerService -> T.Text -> Order -> m OrderId
submitOrder service identity order = do
oid <- nextOrderId service
logDebug "BrokerService" $ "New order, id: " <> (T.pack . show) oid
liftIO $ atomicModifyIORef' (orderMap service) (\s -> (M.insert oid identity s, ()))
r <- liftIO $ Bro.submitOrder (broker service) order { orderId = oid }
case r of
Left err -> logWarning "BrokerService" $ "Submit order error: " <> err
_ -> return ()
return oid
where
nextOrderId srv = liftIO $ atomicModifyIORef' (orderIdCounter srv) (\s -> (s + 1, s))
cancelOrder :: (MonadIO m, WithLog env Message m) => BrokerService -> OrderId -> m ()
cancelOrder service oid = do
r <- liftIO $ Bro.cancelOrder (broker service) oid
case r of
Left err -> logWarning "BrokerServer" $ "Cancel order error: " <> err
_ -> return ()
return ()
getNotifications :: (MonadIO m, WithLog env Message m) => BrokerService -> m [Notification]
getNotifications service = do
v <- liftIO $ Bro.getNotifications (broker service)
case v of
Left err -> do
logWarning "BrokerServer" $ "Get notifications order error: " <> err
return []
Right n -> return n

258
src/ATrade/Driver/Junction/JunctionMonad.hs

@ -1,258 +0,0 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
module ATrade.Driver.Junction.JunctionMonad
(
JunctionEnv(..),
JunctionM(..),
startRobot,
saveRobots,
reloadConfig,
getState,
setState
) where
import ATrade.Broker.Client (BrokerClientHandle)
import ATrade.Driver.Junction.BrokerService (BrokerService)
import ATrade.Driver.Junction.ProgramConfiguration (ProgramConfiguration (logBasePath))
import ATrade.Driver.Junction.QuoteStream (QuoteStream (addSubscription, removeSubscription),
QuoteSubscription (QuoteSubscription))
import ATrade.Driver.Junction.QuoteThread (QuoteThreadHandle)
import qualified ATrade.Driver.Junction.QuoteThread as QT
import ATrade.Driver.Junction.RobotDriverThread (RobotDriverHandle, RobotEnv (RobotEnv),
RobotM (unRobotM),
createRobotDriverThread,
getInstanceDescriptor,
onStrategyInstance,
onStrategyInstanceM)
import ATrade.Driver.Junction.Types (StrategyDescriptorE (StrategyDescriptorE),
StrategyInstanceDescriptor,
accountId,
confStrategy,
confTickers,
configKey,
stateKey,
strategyBaseName,
strategyConfig,
strategyId,
strategyInstanceId,
strategyState,
strategyTimers,
tickerId,
timeframe)
import ATrade.Logging (Message, Severity (Error, Info),
fmtMessage,
logWarning,
logWith)
import ATrade.RoboCom.ConfigStorage (ConfigStorage (loadConfig))
import ATrade.RoboCom.Monad (StrategyEnvironment (..))
import ATrade.RoboCom.Persistence (MonadPersistence (loadState, saveState))
import ATrade.RoboCom.Types (BarSeriesId (BarSeriesId),
Bars,
TickerInfoMap)
import Colog (HasLog (getLogAction, setLogAction),
LogAction,
hoistLogAction,
logTextHandle,
(>$<))
import Control.Exception.Safe (finally)
import Control.Monad.Reader (MonadIO (liftIO),
MonadReader,
ReaderT (runReaderT),
asks)
import Data.Aeson (decode,
eitherDecode,
encode)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Default (Default (def))
import Data.Foldable (traverse_)
import Data.IORef (IORef,
atomicModifyIORef',
newIORef,
readIORef,
writeIORef)
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO (readFile)
import Data.Time (getCurrentTime)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Database.Redis (Connection, get,
mset, runRedis)
import Dhall (auto, input)
import Prelude hiding (log,
readFile)
import System.IO (BufferMode (LineBuffering),
IOMode (AppendMode),
hClose,
hSetBuffering,
openFile)
import System.ZMQ4 (Router, Socket)
import UnliftIO (MonadUnliftIO)
import UnliftIO.Exception (catchAny,
onException)
data JunctionEnv =
JunctionEnv
{
peRedisSocket :: Connection,
peConfigPath :: FilePath,
peQuoteThread :: QuoteThreadHandle,
peBroker :: BrokerClientHandle,
peRobots :: IORef (M.Map T.Text RobotDriverHandle),
peRemoteControlSocket :: Socket Router,
peLogAction :: LogAction JunctionM Message,
peIoLogAction :: LogAction IO Message,
peProgramConfiguration :: ProgramConfiguration,
peBarsMap :: IORef Bars,
peTickerInfoMap :: IORef TickerInfoMap,
peBrokerService :: BrokerService,
peDescriptors :: M.Map T.Text StrategyDescriptorE
}
newtype JunctionM a = JunctionM { unJunctionM :: ReaderT JunctionEnv IO a }
deriving (Functor, Applicative, Monad, MonadReader JunctionEnv, MonadIO, MonadUnliftIO)
instance HasLog JunctionEnv Message JunctionM where
getLogAction = peLogAction
setLogAction a e = e { peLogAction = a }
instance ConfigStorage JunctionM where
loadConfig key = do
basePath <- asks peConfigPath
let path = basePath <> "/" <> T.unpack key -- TODO fix path construction
liftIO $ readFile path >>= input auto
instance MonadPersistence JunctionM where
saveState newState key = do
conn <- asks peRedisSocket
now <- liftIO getPOSIXTime
res <- liftIO $ runRedis conn $ mset [(encodeUtf8 key, BL.toStrict $ encode newState),
(encodeUtf8 (key <> ":last_store") , encodeUtf8 . T.pack . show $ now)]
case res of
Left _ -> logWarning "Junction " "Unable to save state"
Right _ -> return ()
loadState key = do
conn <- asks peRedisSocket
res <- liftIO $ runRedis conn $ get (encodeUtf8 key)
-- TODO: just chain eithers
case res of
Left _ -> do
logWarning "Junction" "Unable to load state"
return def
Right maybeRawState ->
case maybeRawState of
Just rawState -> case eitherDecode $ BL.fromStrict rawState of
Left _ -> do
logWarning "Junction" "Unable to decode state"
return def
Right decodedState -> return decodedState
Nothing -> do
logWarning "Junction" "Unable to decode state"
return def
instance QuoteStream JunctionM where
addSubscription (QuoteSubscription ticker tf) chan = do
qt <- asks peQuoteThread
QT.addSubscription qt ticker tf chan
removeSubscription subId = do
qt <- asks peQuoteThread
QT.removeSubscription qt subId
startRobot :: StrategyInstanceDescriptor -> JunctionM ()
startRobot inst = do
ioLogger <- asks peIoLogAction
descriptors <- asks peDescriptors
cfg <- asks peProgramConfiguration
barsMap <- asks peBarsMap
tickerInfoMap <- asks peTickerInfoMap
broService <- asks peBrokerService
now <- liftIO getCurrentTime
let lLogger = hoistLogAction liftIO ioLogger
logWith lLogger Info "Junction" $ "Starting strategy: " <> strategyBaseName inst
case M.lookup (strategyBaseName inst) descriptors of
Just (StrategyDescriptorE desc) -> flip catchAny (\e -> logWith lLogger Error "Junction" $ "Exception: " <> (T.pack . show $ e)) $ do
bigConf <- loadConfig (configKey inst)
case confTickers bigConf of
(firstTicker:restTickers) -> do
rConf <- liftIO $ newIORef (confStrategy bigConf)
rState <- loadState (stateKey inst) >>= liftIO . newIORef
rTimers <- loadState (stateKey inst <> ":timers") >>= liftIO . newIORef
localH <- liftIO $ openFile (logBasePath cfg <> "/" <> T.unpack (strategyId inst) <> ".log") AppendMode
liftIO $ hSetBuffering localH LineBuffering
let robotLogAction = hoistLogAction liftIO ioLogger <> (fmtMessage >$< logTextHandle localH)
stratEnv <- liftIO $ newIORef StrategyEnvironment
{
_seInstanceId = strategyId inst,
_seAccount = accountId inst,
_seVolume = 1,
_seLastTimestamp = now
}
let robotEnv =
RobotEnv rState rConf rTimers barsMap tickerInfoMap stratEnv robotLogAction broService (toBarSeriesId <$> (firstTicker :| restTickers))
robot <- createRobotDriverThread inst desc (\a -> (flip runReaderT robotEnv . unRobotM) a `finally` hClose localH) bigConf rConf rState rTimers
robotsMap' <- asks peRobots
liftIO $ atomicModifyIORef' robotsMap' (\s -> (M.insert (strategyId inst) robot s, ()))
_ -> logWith lLogger Error (strategyId inst) "No tickers configured !!!"
Nothing -> logWith lLogger Error "Junction" $ "Unknown strategy: " <> strategyBaseName inst
where
toBarSeriesId t = BarSeriesId (tickerId t) (timeframe t)
saveRobots :: JunctionM ()
saveRobots = do
robotsMap <- asks peRobots >>= (liftIO . readIORef)
traverse_ saveRobotState robotsMap
saveRobotState :: RobotDriverHandle -> JunctionM ()
saveRobotState handle = onStrategyInstance handle $ \inst -> do
currentState <- liftIO $ readIORef (strategyState inst)
saveState currentState (strategyInstanceId inst)
currentTimers <- liftIO $ readIORef (strategyTimers inst)
saveState currentTimers (strategyInstanceId inst <> ":timers")
reloadConfig :: T.Text -> JunctionM (Either T.Text ())
reloadConfig instId = flip catchAny (\_ -> return $ Left "Exception") $ do
robotsMap' <- asks peRobots
robots <- liftIO $ readIORef robotsMap'
case M.lookup instId robots of
Just robot -> do
onStrategyInstanceM robot
(\inst -> do
let instDesc = getInstanceDescriptor robot
bigConf <- loadConfig (configKey instDesc)
liftIO $ writeIORef (strategyConfig inst) (confStrategy bigConf))
return $ Right ()
Nothing -> return $ Left "Unable to load config"
getState :: T.Text -> JunctionM (Either T.Text B.ByteString)
getState instId = do
robotsMap' <- asks peRobots
robots <- liftIO $ readIORef robotsMap'
case M.lookup instId robots of
Just robot -> do
Right <$> onStrategyInstanceM robot
(\inst -> do
v <- liftIO $ readIORef (strategyState inst)
return $ BL.toStrict $ encode v)
Nothing -> return $ Left $ "Unknown robot: " <> instId
setState :: T.Text -> B.ByteString -> JunctionM (Either T.Text ())
setState instId newState = do
robotsMap' <- asks peRobots
robots <- liftIO $ readIORef robotsMap'
case M.lookup instId robots of
Just robot -> do
onStrategyInstanceM robot
(\inst -> do
case decode . BL.fromStrict $ newState of
Just newS -> do
liftIO $ writeIORef (strategyState inst) newS
return $ Right ()
Nothing -> return $ Left $ "Unable to decode state for " <> instId)
Nothing -> return $ Left $ "Unknown robot: " <> instId

72
src/ATrade/Driver/Junction/ProgramConfiguration.hs

@ -1,72 +0,0 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module ATrade.Driver.Junction.ProgramConfiguration
(
ProgramOptions(..),
ProgramConfiguration(..)
) where
import ATrade.Driver.Junction.Types (StrategyInstanceDescriptor)
import ATrade.Logging (Severity (..))
import qualified Data.Text as T
import Dhall (FromDhall, autoWith)
import Dhall.Core (Expr (..), FieldSelection (..))
import qualified Dhall.Map
import Dhall.Marshal.Decode (Decoder (..), typeError)
import GHC.Generics (Generic)
newtype ProgramOptions =
ProgramOptions
{
configPath :: FilePath
}
data ProgramConfiguration =
ProgramConfiguration
{
brokerEndpoint :: T.Text,
brokerNotificationEndpoint :: T.Text,
brokerServerCert :: Maybe FilePath,
brokerClientCert :: Maybe FilePath,
brokerIdentity :: T.Text,
quotesourceEndpoint :: T.Text,
quotesourceServerCert :: Maybe FilePath,
quotesourceClientCert :: Maybe FilePath,
qhpEndpoint :: T.Text,
qtisEndpoint :: T.Text,
remoteControlEndpoint :: T.Text,
redisSocket :: T.Text,
robotsConfigsPath :: FilePath,
logBasePath :: FilePath,
logLevels :: [(T.Text, Severity)],
instances :: [StrategyInstanceDescriptor]
} deriving (Generic, Show)
instance FromDhall Severity where
autoWith _ = Decoder {..}
where
extract expr@(Field _ FieldSelection{ fieldSelectionLabel }) =
case fieldSelectionLabel of
"Trace" -> pure Trace
"Debug" -> pure Debug
"Info" -> pure Info
"Warning" -> pure Warning
"Error" -> pure Error
_ -> typeError expected expr
extract expr = typeError expected expr
expected = pure
(Union
(Dhall.Map.fromList
[ ("Trace", Nothing)
, ("Debug", Nothing)
, ("Info", Nothing)
, ("Warning", Nothing)
, ("Error", Nothing)
]
)
)
instance FromDhall ProgramConfiguration

30
src/ATrade/Driver/Junction/QuoteStream.hs

@ -1,30 +0,0 @@
{-# LANGUAGE DeriveGeneric #-}
module ATrade.Driver.Junction.QuoteStream
(
QuoteSubscription(..),
QuoteStream(..),
SubscriptionId(..)
) where
import ATrade.QuoteSource.Client (QuoteData)
import ATrade.Types (BarTimeframe, TickerId)
import Control.Concurrent.BoundedChan (BoundedChan)
import Data.Hashable (Hashable)
import GHC.Generics (Generic)
data QuoteSubscription =
QuoteSubscription TickerId BarTimeframe
deriving (Generic, Eq)
instance Hashable BarTimeframe
instance Hashable QuoteSubscription
newtype SubscriptionId = SubscriptionId { unSubscriptionId :: Int }
deriving (Show, Eq, Generic)
instance Hashable SubscriptionId
class (Monad m) => QuoteStream m where
addSubscription :: QuoteSubscription -> BoundedChan QuoteData -> m SubscriptionId
removeSubscription :: SubscriptionId -> m ()

304
src/ATrade/Driver/Junction/QuoteThread.hs

@ -1,304 +0,0 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module ATrade.Driver.Junction.QuoteThread
(
QuoteThreadHandle,
startQuoteThread,
stopQuoteThread,
addSubscription,
removeSubscription,
DownloaderM,
DownloaderEnv(..),
runDownloaderM,
withQThread
) where
import qualified ATrade.BarAggregator as BA
import ATrade.Driver.Junction.ProgramConfiguration (ProgramConfiguration (..))
import ATrade.Driver.Junction.QuoteStream (QuoteSubscription (..),
SubscriptionId (SubscriptionId))
import ATrade.Logging (Message, logDebug,
logInfo,
logWarning)
import ATrade.Quotes.HistoryProvider (HistoryProvider (..))
import ATrade.Quotes.QHP (QHPHandle, requestHistoryFromQHP)
import ATrade.Quotes.QTIS (TickerInfo (tiLotSize, tiTickSize, tiTicker),
qtisGetTickersInfo)
import ATrade.Quotes.TickerInfoProvider (TickerInfoProvider (..))
import ATrade.QuoteSource.Client (QuoteData (QDBar, QDTick),
QuoteSourceClientHandle,
quoteSourceClientSubscribe,
startQuoteSourceClient,
stopQuoteSourceClient)
import ATrade.RoboCom.Types (Bar (barSecurity),
BarSeries (..),
BarSeriesId (BarSeriesId),
Bars,
InstrumentParameters (InstrumentParameters),
TickerInfoMap)
import ATrade.Types (BarTimeframe (BarTimeframe),
ClientSecurityParams (ClientSecurityParams),
Tick (security),
TickerId)
import Colog (HasLog (getLogAction, setLogAction),
LogAction,
WithLog)
import Control.Concurrent (ThreadId, forkIO,
killThread)
import Control.Concurrent.BoundedChan (BoundedChan,
newBoundedChan,
readChan,
tryWriteChan,
writeChan)
import Control.Exception.Safe (MonadMask,
MonadThrow,
bracket)
import Control.Monad (forM, forM_,
forever)
import Control.Monad.Reader (MonadIO (liftIO), ReaderT (runReaderT),
lift)
import Control.Monad.Reader.Class (MonadReader, asks)
import qualified Data.HashMap.Strict as HM
import Data.IORef (IORef,
atomicModifyIORef',
newIORef,
readIORef)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Time (addUTCTime,
getCurrentTime)
import System.ZMQ4 (Context)
import System.ZMQ4.ZAP (loadCertificateFromFile)
data QuoteThreadHandle = QuoteThreadHandle ThreadId ThreadId QuoteThreadEnv
data QuoteThreadEnv =
QuoteThreadEnv
{
bars :: IORef Bars,
endpoints :: IORef (HM.HashMap QuoteSubscription [(SubscriptionId, BoundedChan QuoteData)]),
qsclient :: QuoteSourceClientHandle,
paramsCache :: IORef TickerInfoMap,
downloaderChan :: BoundedChan QuoteSubscription,
subscriptionIdCounter :: IORef Int,
subscriptions :: IORef (HM.HashMap SubscriptionId QuoteSubscription),
aggregators :: IORef (HM.HashMap (TickerId, BarTimeframe) BA.BarAggregator)
}
startQuoteThread :: (MonadIO m,
MonadIO m1,
WithLog env Message m1,
HistoryProvider m1,
TickerInfoProvider m1) =>
IORef Bars ->
IORef TickerInfoMap ->
Context ->
T.Text ->
ClientSecurityParams ->
(m1 () -> IO ()) ->
LogAction IO Message ->
m QuoteThreadHandle
startQuoteThread barsRef tiRef ctx ep secparams downloadThreadRunner logger = do
chan <- liftIO $ newBoundedChan 2000
dChan <- liftIO $ newBoundedChan 2000
qsc <- liftIO $ startQuoteSourceClient chan [] ctx ep secparams logger
env <- liftIO $ QuoteThreadEnv barsRef <$> newIORef HM.empty <*> pure qsc <*> pure tiRef <*> pure dChan <*> newIORef 0 <*> newIORef HM.empty <*> newIORef HM.empty
tid <- liftIO . forkIO $ quoteThread env chan
downloaderTid <- liftIO . forkIO $ downloadThreadRunner (downloaderThread env dChan)
return $ QuoteThreadHandle tid downloaderTid env
where
downloaderThread env chan = do
logInfo "QuoteThread" "Started"
forever $ do
QuoteSubscription tickerid tf <- liftIO $ readChan chan
logInfo "QuoteThread" $ "Subscription: " <> tickerid
paramsMap <- liftIO $ readIORef $ paramsCache env
mbParams <- case M.lookup tickerid paramsMap of
Nothing -> do
paramsList <- getInstrumentParameters [tickerid]
case paramsList of
(params:_) -> liftIO $ atomicModifyIORef' (paramsCache env) (\m -> (M.insert tickerid params m, Just params))
_ -> return Nothing
Just params -> return $ Just params
logDebug "QuoteThread" $ "Got info params: " <> (T.pack . show $ mbParams)
barsMap <- liftIO $ readIORef (bars env)
case M.lookup (BarSeriesId tickerid tf) barsMap of
Just _ -> return () -- already downloaded
Nothing -> case mbParams of
Just params -> do
now <- liftIO getCurrentTime
-- Load data in interval [today - 60days; today + 1day]. +1 day guarantees that we will download data up until current time.
-- If we don't make this adjustment it is possible that we will get data only up to beginning of current day.
barsData <- getHistory tickerid tf ((-86400 * 60) `addUTCTime` now) (86400 `addUTCTime` now)
let barSeries = BarSeries tickerid tf barsData params
liftIO $ atomicModifyIORef' (bars env) (\m -> (M.insert (BarSeriesId tickerid tf) barSeries m, ()))
_ -> logWarning "QuoteThread" $ "Unable to find parameters for: " <> (T.pack . show $ BarSeriesId tickerid tf)
pushToBarAggregators tick = forM_ (BarTimeframe <$> [60, 300, 900, 3600]) (pushTickToAggregator tick)
pushTickToAggregator tick tf = do
aggsRef <- asks aggregators
aggs <- liftIO . readIORef $ aggsRef
let key = (security tick, tf)
case HM.lookup key aggs of
Just agg -> do
let (mbar, agg') = BA.handleTick tick agg
liftIO $ atomicModifyIORef' aggsRef (\m -> (HM.insert key agg' m, ()))
barsRef' <- asks bars
case mbar of
Just bar -> do
liftIO $ atomicModifyIORef' barsRef' (\x -> (updateBarsMap x bar tf, ()))
writeBarData bar tf (QDBar (tf, bar))
_ -> do
pure ()
_ -> do
let agg = BA.mkAggregatorFromBars (M.singleton (security tick) (BarSeries (security tick) tf [] (InstrumentParameters (security tick) 1 1))) [(0, 86400)]
liftIO $ atomicModifyIORef' aggsRef (\m -> (HM.insert key agg m, ()))
quoteThread env chan = flip runReaderT env $ forever $ do
qssData <- lift $ readChan chan
case qssData of
QDBar (tf, bar) -> do
barsRef' <- asks bars
lift $ atomicModifyIORef' barsRef' (\x -> (updateBarsMap x bar tf, ()))
writeBarData bar tf qssData
QDTick tick -> do
pushToBarAggregators tick
writeTickData tick qssData
writeTickData tick qssData = do
let key = QuoteSubscription (security tick) (BarTimeframe 0)
subs <- asks endpoints >>= (lift . readIORef)
case HM.lookup key subs of
Just clientChannels -> do
lift $ mapM_ (\(_, chan') -> tryWriteChan chan' qssData) clientChannels
Nothing -> return ()
writeBarData bar tf qssData = do
let key = QuoteSubscription (barSecurity bar) tf
subs <- asks endpoints >>= (lift . readIORef)
case HM.lookup key subs of
Just clientChannels -> do
lift $ mapM_ (\(_, chan') -> tryWriteChan chan' qssData) clientChannels
Nothing -> return ()
stopQuoteThread :: (MonadIO m) => QuoteThreadHandle -> m ()
stopQuoteThread (QuoteThreadHandle tid dtid env) = liftIO $ do
killThread tid
killThread dtid
stopQuoteSourceClient (qsclient env)
addSubscription :: (MonadIO m) => QuoteThreadHandle -> TickerId -> BarTimeframe -> BoundedChan QuoteData -> m SubscriptionId
addSubscription (QuoteThreadHandle _ _ env) tid tf chan = liftIO $ do
cnt <- atomicModifyIORef' (subscriptionIdCounter env) (\c -> (c + 1, c))
let subscription = QuoteSubscription tid tf
let subid = SubscriptionId cnt
writeChan (downloaderChan env) subscription
atomicModifyIORef' (endpoints env) (\m -> (doAddSubscription m subid tid, ()))
atomicModifyIORef' (subscriptions env) (\m -> (HM.insert subid subscription m, ()))
quoteSourceClientSubscribe (qsclient env) [(tid, BarTimeframe 0)]
return subid
where
doAddSubscription m subid tickerid =
let m1 = HM.alter (\case
Just chans -> Just ((subid, chan) : chans)
_ -> Just [(subid, chan)]) (QuoteSubscription tickerid tf) m in
HM.alter (\case
Just chans -> Just ((subid, chan) : chans)
_ -> Just [(subid, chan)]) (QuoteSubscription tickerid (BarTimeframe 0)) m1
removeSubscription :: (MonadIO m) => QuoteThreadHandle -> SubscriptionId -> m ()
removeSubscription (QuoteThreadHandle _ _ env) subId = liftIO $ do
subs <- readIORef (subscriptions env)
case HM.lookup subId subs of
Just sub -> atomicModifyIORef' (endpoints env) (\m -> (doRemoveSubscription m sub, ()))
Nothing -> return ()
where
doRemoveSubscription m sub =
let m1 = HM.adjust (filter (\(subId', _) -> subId' == subId)) sub m in
HM.adjust (filter (\(subId', _) -> subId' == subId)) (sub0 sub) m1
sub0 sub = let QuoteSubscription tid _ = sub in QuoteSubscription tid (BarTimeframe 0)
updateBarsMap :: Bars -> Bar -> BarTimeframe -> Bars
updateBarsMap barsMap bar tf = M.adjust (addToSeries bar) (BarSeriesId (barSecurity bar) tf) barsMap
addToSeries :: Bar -> BarSeries -> BarSeries
addToSeries bar series = series { bsBars = bar : bsBars series }
data DownloaderEnv =
DownloaderEnv
{
qhp :: QHPHandle,
downloaderContext :: Context,
downloaderQtisEndpoint :: T.Text,
logAction :: LogAction DownloaderM Message
}
newtype DownloaderM a = DownloaderM { unDownloaderM :: ReaderT DownloaderEnv IO a }
deriving (Functor, Applicative, Monad, MonadReader DownloaderEnv, MonadIO, MonadThrow)
instance HasLog DownloaderEnv Message DownloaderM where
getLogAction = logAction
setLogAction a e = e { logAction = a }
instance HistoryProvider DownloaderM where
getHistory tid tf from to = do
q <- asks qhp
requestHistoryFromQHP q tid tf from to
instance TickerInfoProvider DownloaderM where
getInstrumentParameters tickers = do
ctx <- asks downloaderContext
ep <- asks downloaderQtisEndpoint
tis <- forM tickers (qtisGetTickersInfo ctx ep)
pure $ convert `fmap` tis
where
convert ti = InstrumentParameters
(tiTicker ti)
(fromInteger $ tiLotSize ti)
(tiTickSize ti)
withQThread ::
DownloaderEnv
-> IORef Bars
-> IORef TickerInfoMap
-> ProgramConfiguration
-> Context
-> LogAction IO Message
-> (QuoteThreadHandle -> IO ())
-> IO ()
withQThread env barsMap tiMap cfg ctx logger f = do
securityParameters <- loadSecurityParameters
bracket
(startQuoteThread
barsMap
tiMap
ctx
(quotesourceEndpoint cfg)
securityParameters
(runDownloaderM env)
logger)
stopQuoteThread f
where
loadSecurityParameters =
case (quotesourceClientCert cfg, quotesourceServerCert cfg) of
(Just clientCertPath, Just serverCertPath) -> do
eClientCert <- loadCertificateFromFile clientCertPath
eServerCert <- loadCertificateFromFile serverCertPath
case (eClientCert, eServerCert) of
(Right clientCert, Right serverCert) -> return $ ClientSecurityParams (Just clientCert) (Just serverCert)
(_, _) -> return $ ClientSecurityParams Nothing Nothing
_ -> return $ ClientSecurityParams Nothing Nothing
runDownloaderM :: DownloaderEnv -> DownloaderM () -> IO ()
runDownloaderM env = (`runReaderT` env) . unDownloaderM

151
src/ATrade/Driver/Junction/RemoteControl.hs

@ -1,151 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
module ATrade.Driver.Junction.RemoteControl
(
handleRemoteControl
) where
import ATrade.Driver.Junction.JunctionMonad (JunctionEnv (peLogAction, peRemoteControlSocket, peRobots),
JunctionM, getState,
reloadConfig,
setState, startRobot)
import ATrade.Driver.Junction.RobotDriverThread (stopRobot)
import ATrade.Driver.Junction.Types (StrategyInstanceDescriptor)
import ATrade.Logging (Severity (Info),
logErrorWith,
logWith)
import Control.Monad (unless)
import Control.Monad.Reader (asks)
import Data.Aeson (decode)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8',
encodeUtf8)
import System.ZMQ4 (Event (In),
Poll (Sock), poll,
receiveMulti,
sendMulti)
import UnliftIO (MonadIO (liftIO),
atomicModifyIORef',
readIORef)
data RemoteControlResponse =
ResponseOk
| ResponseError T.Text
| ResponseData B.ByteString
deriving (Show, Eq)
data RemoteControlRequest =
StartRobot StrategyInstanceDescriptor
| StopRobot T.Text
| ReloadConfig T.Text
| GetState T.Text
| SetState T.Text B.ByteString
| Ping
deriving (Show)
data ParseError =
UnknownCmd
| UtfDecodeError
| JsonDecodeError
deriving (Show, Eq)
parseRemoteControlRequest :: B.ByteString -> Either ParseError RemoteControlRequest
parseRemoteControlRequest bs =
if
| cmd == "START" -> parseStart
| cmd == "STOP" -> parseStop
| cmd == "RELOAD_CONFIG" -> parseReloadConfig
| cmd == "GET_STATE" -> parseGetState
| cmd == "SET_STATE" -> parseSetState
| cmd == "PING" -> Right Ping
| otherwise -> Left UnknownCmd
where
cmd = B.takeWhile (/= 0x20) bs
rest = B.dropWhile (== 0x20) . B.dropWhile (/= 0x20) $ bs
parseStart = case decode . BL.fromStrict $ rest of
Just inst -> Right (StartRobot inst)
Nothing -> Left JsonDecodeError
parseStop = case decodeUtf8' rest of
Left _ -> Left UtfDecodeError
Right r -> Right (StopRobot (T.strip r))
parseReloadConfig = case decodeUtf8' rest of
Left _ -> Left UtfDecodeError
Right r -> Right (ReloadConfig (T.strip r))
parseGetState = case decodeUtf8' (B.takeWhile (/= 0x20) rest) of
Left _ -> Left UtfDecodeError
Right r -> Right (GetState r)
parseSetState = case decodeUtf8' (B.takeWhile (/= 0x20) rest) of
Left _ -> Left UtfDecodeError
Right r -> Right (SetState r (B.dropWhile (== 0x20) . B.dropWhile (/= 0x20) $ rest))
makeRemoteControlResponse :: RemoteControlResponse -> B.ByteString
makeRemoteControlResponse ResponseOk = "OK"
makeRemoteControlResponse (ResponseError msg) = "ERROR " <> encodeUtf8 msg
makeRemoteControlResponse (ResponseData d) = "DATA\n" <> d
handleRemoteControl :: Int -> JunctionM ()
handleRemoteControl timeout = do
sock <- asks peRemoteControlSocket
logger <- asks peLogAction
evs <- poll (fromIntegral timeout) [Sock sock [In] Nothing]
case evs of
(x:_) -> unless (null x) $ do
frames <- liftIO $ receiveMulti sock
case frames of
[peerId, _, rawRequest] -> do
case parseRemoteControlRequest rawRequest of
Left err -> logErrorWith logger "RemoteControl" ("Unable to parse request: " <> (T.pack . show) err)
Right request -> do
response <- handleRequest request
liftIO $ sendMulti sock $ peerId :| [B.empty, makeRemoteControlResponse response]
_ -> logErrorWith logger "RemoteControl" "Invalid incoming request"
_ -> return ()
where
handleRequest (StartRobot inst) = do
startRobot inst
return ResponseOk
handleRequest (StopRobot instId) = do
robotsRef <- asks peRobots
robots <- readIORef robotsRef
case M.lookup instId robots of
Just robot -> do
logger <- asks peLogAction
logWith logger Info "RemoteControl" $ "Stopping robot: " <> instId
stopRobot robot
liftIO $ atomicModifyIORef' robotsRef (\r -> (M.delete instId r, ()))
return ResponseOk
Nothing -> return $ ResponseError $ "Not started: " <> instId
handleRequest (ReloadConfig instId) = do
res <- reloadConfig instId
case res of
Left errmsg -> return $ ResponseError errmsg
Right () -> return ResponseOk
handleRequest (GetState instId) = do
res <- getState instId
case res of
Left errmsg -> return $ ResponseError errmsg
Right d -> return $ ResponseData d
handleRequest (SetState instId rawState) = do
res <- setState instId rawState
case res of
Left errmsg -> return $ ResponseError errmsg
Right () -> return ResponseOk
handleRequest Ping = return ResponseOk

216
src/ATrade/Driver/Junction/RobotDriverThread.hs

@ -1,216 +0,0 @@
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Junction.RobotDriverThread
(
createRobotDriverThread,
RobotEnv(..),
RobotM(..),
RobotDriverHandle,
onStrategyInstance,
onStrategyInstanceM,
postNotificationEvent,
stopRobot,
getInstanceDescriptor
) where
import ATrade.Broker.Protocol (Notification (OrderNotification, TradeNotification))
import qualified ATrade.Driver.Junction.BrokerService as Bro
import ATrade.Driver.Junction.QuoteStream (QuoteStream (addSubscription, removeSubscription),
QuoteSubscription (QuoteSubscription),
SubscriptionId)
import ATrade.Driver.Junction.Types (BigConfig,
StrategyDescriptor,
StrategyInstance (StrategyInstance, strategyEventCallback),
StrategyInstanceDescriptor (configKey),
confStrategy,
confTickers,
eventCallback, stateKey,
strategyId, tickerId,
timeframe)
import ATrade.Logging (Message, log)
import ATrade.QuoteSource.Client (QuoteData (..))
import ATrade.RoboCom.ConfigStorage (ConfigStorage)
import ATrade.RoboCom.Monad (Event (NewBar, NewTick, NewTrade, OrderUpdate),
MonadRobot (..),
StrategyEnvironment (..))
import ATrade.RoboCom.Persistence (MonadPersistence)
import ATrade.RoboCom.Types (BarSeriesId (BarSeriesId),
Bars, TickerInfoMap)
import ATrade.Types (OrderId, OrderState,
Tick (value), Trade)
import Colog (HasLog (getLogAction, setLogAction),
LogAction)
import Control.Concurrent (ThreadId, forkIO,
killThread)
import Control.Concurrent.BoundedChan (BoundedChan,
newBoundedChan, readChan,
writeChan)
import Control.Exception.Safe (MonadThrow)
import Control.Monad (forM, forM_, forever,
void, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (MonadReader (local),
ReaderT, asks)
import Data.Aeson (FromJSON, ToJSON)
import Data.Default (Default)
import Data.IORef (IORef,
atomicModifyIORef',
readIORef, writeIORef)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Map.Strict as M
import qualified Data.Text.Lazy as TL
import Data.Time (UTCTime, getCurrentTime)
import Dhall (FromDhall)
import Prelude hiding (log)
data RobotDriverHandle = forall c s. (FromDhall c, Default s, FromJSON s, ToJSON s) =>
RobotDriverHandle StrategyInstanceDescriptor (StrategyInstance c s) ThreadId ThreadId (BoundedChan RobotDriverEvent) [SubscriptionId]
data RobotDriverRequest
data RobotDriverEvent =
EventRequest RobotDriverRequest
| QuoteEvent QuoteData
| NewTradeEvent Trade
| OrderEvent OrderId OrderState
robotDriverThread :: (MonadIO m,
MonadRobot m c s) =>
StrategyInstance c s ->
BoundedChan RobotDriverEvent ->
m ()
robotDriverThread inst eventQueue =
forever $ liftIO (readChan eventQueue) >>= handleEvent
where
handleEvent (EventRequest _) = return ()
handleEvent (QuoteEvent d) =
case d of
QDTick tick -> when (value tick /= 0) $ strategyEventCallback inst (NewTick tick)
QDBar (tf, bar) -> strategyEventCallback inst (NewBar (tf, bar))
handleEvent (NewTradeEvent trade) = strategyEventCallback inst (NewTrade trade)
handleEvent (OrderEvent oid newState) = strategyEventCallback inst (OrderUpdate oid newState)
createRobotDriverThread :: (MonadIO m1,
ConfigStorage m1,
MonadPersistence m1,
QuoteStream m1,
Default s,
FromJSON s,
ToJSON s,
FromDhall c,
MonadIO m,
MonadReader (RobotEnv c s) m,
MonadRobot m c s) =>
StrategyInstanceDescriptor
-> StrategyDescriptor c s
-> (m () -> IO ())
-> BigConfig c
-> IORef c
-> IORef s
-> IORef [UTCTime]
-> m1 RobotDriverHandle
createRobotDriverThread instDesc strDesc runner bigConf rConf rState rTimers = do
eventQueue <- liftIO $ newBoundedChan 2000
let inst = StrategyInstance (strategyId instDesc) (eventCallback strDesc) rState rConf rTimers
quoteQueue <- liftIO $ newBoundedChan 2000
subIds <- forM (confTickers bigConf) (\x -> addSubscription (QuoteSubscription (tickerId x) (timeframe x)) quoteQueue)
qthread <- liftIO . forkIO $ forever $ passQuoteEvents eventQueue quoteQueue
driver <- liftIO . forkIO $ runner $ robotDriverThread inst eventQueue
return $ RobotDriverHandle instDesc inst driver qthread eventQueue subIds
where
passQuoteEvents eventQueue quoteQueue = do
v <- readChan quoteQueue
writeChan eventQueue (QuoteEvent v)
stopRobot :: (MonadIO m, QuoteStream m) => RobotDriverHandle -> m ()
stopRobot (RobotDriverHandle _ _ driver qthread _ subIds) = do
forM_ subIds removeSubscription
liftIO $ killThread driver
liftIO $ killThread qthread
onStrategyInstance :: RobotDriverHandle -> forall r. (forall c s. (FromDhall c, Default s, FromJSON s, ToJSON s) => StrategyInstance c s -> r) -> r
onStrategyInstance (RobotDriverHandle _ inst _ _ _ _) f = f inst
onStrategyInstanceM :: (MonadIO m) => RobotDriverHandle ->
(forall c s. (FromDhall c, Default s, FromJSON s, ToJSON s) => StrategyInstance c s -> m r) -> m r
onStrategyInstanceM (RobotDriverHandle _ inst _ _ _ _) f = f inst
data RobotEnv c s =
RobotEnv
{
stateRef :: IORef s,
configRef :: IORef c,
timersRef :: IORef [UTCTime],
bars :: IORef Bars,
tickerInfoMap :: IORef TickerInfoMap,
env :: IORef StrategyEnvironment,
logAction :: LogAction (RobotM c s) Message,
brokerService :: Bro.BrokerService,
tickers :: NonEmpty BarSeriesId
}
newtype RobotM c s a = RobotM { unRobotM :: ReaderT (RobotEnv c s) IO a }
deriving (Functor, Applicative, Monad, MonadReader (RobotEnv c s), MonadIO, MonadThrow)
instance HasLog (RobotEnv c s) Message (RobotM c s) where
getLogAction = logAction
setLogAction a e = e { logAction = a }
instance MonadRobot (RobotM c s) c s where
submitOrder order = do
instId <- _seInstanceId <$> (asks env >>= liftIO . readIORef)
bro <- asks brokerService
Bro.submitOrder bro instId order
cancelOrder oid = do
bro <- asks brokerService
Bro.cancelOrder bro oid
appendToLog s t = do
instId <- _seInstanceId <$> (asks env >>= liftIO . readIORef)
log s instId $ TL.toStrict t
setupTimer t = do
ref <- asks timersRef
liftIO $ atomicModifyIORef' ref (\s -> (t : s, ()))
enqueueIOAction = undefined
getConfig = asks configRef >>= liftIO . readIORef
getState = asks stateRef >>= liftIO . readIORef
setState newState = asks stateRef >>= liftIO . flip writeIORef newState
getEnvironment = do
ref <- asks env
now <- liftIO getCurrentTime
liftIO $ atomicModifyIORef' ref (\e -> (e { _seLastTimestamp = now }, e { _seLastTimestamp = now}))
getTicker tid tf = do
b <- asks bars >>= liftIO . readIORef
return $ M.lookup (BarSeriesId tid tf) b
getTickerInfo tid = do
b <- asks tickerInfoMap >>= liftIO . readIORef
return $ M.lookup tid b
getAvailableTickers = asks tickers
postNotificationEvent :: (MonadIO m) => RobotDriverHandle -> Notification -> m ()
postNotificationEvent (RobotDriverHandle _ _ _ _ eventQueue _) notification = liftIO $
case notification of
OrderNotification _ oid state -> writeChan eventQueue (OrderEvent oid state)
TradeNotification _ trade -> writeChan eventQueue (NewTradeEvent trade)
getInstanceDescriptor :: RobotDriverHandle -> StrategyInstanceDescriptor
getInstanceDescriptor (RobotDriverHandle instDesc _ _ _ _ _) = instDesc

70
src/ATrade/Driver/Junction/Types.hs

@ -1,7 +1,4 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Junction.Types module ATrade.Driver.Junction.Types
@ -9,84 +6,49 @@ module ATrade.Driver.Junction.Types
StrategyDescriptor(..), StrategyDescriptor(..),
TickerConfig(..), TickerConfig(..),
StrategyInstanceDescriptor(..), StrategyInstanceDescriptor(..),
StrategyInstance(..), StrategyInstance(..)
BigConfig(..),
StrategyDescriptorE(..),
StrategyInstanceE(..)
) where ) where
import ATrade.RoboCom.Monad (EventCallback) import ATrade.RoboCom.Monad (EventCallback)
import ATrade.Types (BarTimeframe (..), TickerId) import ATrade.Types (BarTimeframe, TickerId)
import Data.Aeson (FromJSON (..), ToJSON (..), withObject, import Data.Aeson (FromJSON (..), ToJSON (..))
(.:)) import qualified Data.ByteString as B
import Data.Default (Default) import Data.IORef
import Data.IORef (IORef)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time (UTCTime)
import Dhall (FromDhall, autoWith, natural)
import GHC.Generics (Generic)
data StrategyDescriptor c s = data StrategyDescriptor =
forall c s. (FromJSON s, ToJSON s, FromJSON c) =>
StrategyDescriptor StrategyDescriptor
{ {
baseStrategyName :: T.Text, baseStrategyName :: T.Text,
eventCallback :: EventCallback c s eventCallback :: EventCallback c s,
defaultState :: s
} }
data StrategyDescriptorE = forall c s. (FromDhall c, Default s, FromJSON s, ToJSON s) => StrategyDescriptorE (StrategyDescriptor c s)
data TickerConfig = data TickerConfig =
TickerConfig TickerConfig
{ {
tickerId :: TickerId, tickerId :: TickerId,
timeframe :: BarTimeframe timeframe :: BarTimeframe
} }
deriving (Generic)
instance FromDhall BarTimeframe where
autoWith _ = BarTimeframe . fromIntegral <$> natural
instance FromDhall TickerConfig
data BigConfig c = BigConfig {
confTickers :: [TickerConfig],
confStrategy :: c
} deriving (Generic)
instance (FromDhall c) => FromDhall (BigConfig c)
data StrategyInstanceDescriptor = data StrategyInstanceDescriptor =
StrategyInstanceDescriptor StrategyInstanceDescriptor
{ {
accountId :: T.Text,
strategyId :: T.Text, strategyId :: T.Text,
strategyBaseName :: T.Text, strategyName :: T.Text,
configKey :: T.Text, configKey :: T.Text,
stateKey :: T.Text, stateKey :: T.Text,
logPath :: T.Text logPath :: T.Text,
} deriving (Generic, Show) tickers :: [TickerConfig]
}
instance FromDhall StrategyInstanceDescriptor
instance FromJSON StrategyInstanceDescriptor where
parseJSON = withObject "StrategyInstanceDescriptor" $ \obj ->
StrategyInstanceDescriptor <$>
obj .: "account_id" <*>
obj .: "strategy_id" <*>
obj .: "strategy_base_name" <*>
obj .: "config_key" <*>
obj .: "state_key" <*>
obj .: "log_path"
data StrategyInstance c s = data StrategyInstance =
forall c s. (FromJSON s, ToJSON s, FromJSON c) =>
StrategyInstance StrategyInstance
{ {
strategyInstanceId :: T.Text, strategyInstanceId :: T.Text,
strategyEventCallback :: EventCallback c s, strategyEventCallback :: EventCallback c s,
strategyState :: IORef s, strategyState :: IORef s,
strategyConfig :: IORef c, strategyConfig :: IORef c
strategyTimers :: IORef [UTCTime]
} }
data StrategyInstanceE = forall c s. (FromDhall c, Default s, FromJSON s, ToJSON s) => StrategyInstanceE (StrategyInstance c s)

92
src/ATrade/Driver/Real.hs

@ -1,7 +1,10 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Real ( module ATrade.Driver.Real (
@ -11,45 +14,56 @@ module ATrade.Driver.Real (
barStrategyDriver barStrategyDriver
) where ) where
import Options.Applicative import ATrade.BarAggregator
import System.IO import ATrade.Driver.Real.BrokerClientThread
import System.Signal import ATrade.Driver.Real.QuoteSourceThread
import System.Exit import ATrade.Driver.Types (InitializationCallback, StrategyInstanceParams (..))
import System.Random import ATrade.Exceptions
import System.Log.Logger import ATrade.Quotes (MonadHistory (..), MonadInstrumentParametersSource (..))
import System.Log.Handler.Simple import ATrade.Quotes.QHP as QQ
import System.Log.Handler (setFormatter) import ATrade.Quotes.QTIS (TickerInfo (..),
import System.Log.Formatter qtisGetTickersInfo)
import Control.Monad import ATrade.RoboCom.Monad (Event (..),
import Control.Concurrent hiding (writeChan, readChan, writeList2Chan, yield) EventCallback,
MonadRobot (..),
StrategyEnvironment (..),
seBars, seLastTimestamp)
import ATrade.RoboCom.Types (BarSeries (..), InstrumentParameters (..),
Ticker (..),
Timeframe (..))
import ATrade.RoboCom.Utils (fromHMS)
import ATrade.Types
import Control.Concurrent hiding (readChan,
writeChan,
writeList2Chan, yield)
import Control.Concurrent.BoundedChan as BC import Control.Concurrent.BoundedChan as BC
import Control.Exception import Control.Exception.Safe
import Control.Lens hiding (Context, (.=))
import Control.Monad
import Control.Monad.Reader
import Data.Aeson
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.List as L import Data.IORef
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding import Data.Text.Encoding
import Data.Aeson import qualified Data.Text.Lazy as TL
import Data.IORef
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Data.Maybe import Database.Redis hiding (decode, info)
import Data.Monoid import GHC.Generics
import Database.Redis hiding (info, decode) import Options.Applicative
import ATrade.Types import System.Exit
import ATrade.RoboCom.Monad (StrategyMonad, StrategyAction(..), EventCallback, Event(..), runStrategyElement, StrategyEnvironment(..), Event(..)) import System.IO
import ATrade.BarAggregator import System.Log.Formatter
import ATrade.Driver.Real.BrokerClientThread import System.Log.Handler (setFormatter)
import ATrade.Driver.Real.QuoteSourceThread import System.Log.Handler.Simple
import ATrade.Driver.Real.Types (Strategy(..), StrategyInstanceParams(..), InitializationCallback) import System.Log.Logger
import ATrade.RoboCom.Types (BarSeries(..), Ticker(..), Timeframe(..)) import System.Signal
import ATrade.Exceptions import System.ZMQ4 hiding (Event (..))
import ATrade.Quotes.Finam as QF
import ATrade.Quotes.QHP as QQ
import ATrade.Quotes.HAP as QH
import System.ZMQ4 hiding (Event(..))
data Params = Params { data Params = Params {
instanceId :: String, instanceId :: String,
@ -408,18 +422,6 @@ barStrategyDriver downloadDelta instanceParams callback shutdownVar = do
nowRef <- asks envLastTimestamp nowRef <- asks envLastTimestamp
lift $ writeIORef nowRef newTimestamp lift $ writeIORef nowRef newTimestamp
newTimers <- catMaybes <$> (readIORef timersRef >>= mapM (checkTimer eventChan newTimestamp))
atomicWriteIORef timersRef newTimers
let !newenv = env { seBars = currentBars, seLastTimestamp = newTimestamp }
let (!newState, !actions, _) = runStrategyElement params curState newenv $ (eventCallback strategy) event
writeIORef stateRef newState
writeIORef timersRef newTimers
newTimers' <- catMaybes <$> mapM handleTimerActions actions
mapM_ (handleActions ordersChan) actions
readAndHandleEvents agg ordersChan eventChan (strategy' { currentState = newState, strategyTimers = newTimers ++ newTimers' }) newenv
else debugM "Strategy" "Shutdown requested"
timersRef <- asks envTimers timersRef <- asks envTimers
oldTimers <- lift $ readIORef timersRef oldTimers <- lift $ readIORef timersRef
newTimers <- catMaybes <$> mapM (checkTimer eventChan newTimestamp) oldTimers newTimers <- catMaybes <$> mapM (checkTimer eventChan newTimestamp) oldTimers

361
src/ATrade/Quotes/Finam.hs

@ -0,0 +1,361 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances #-}
module ATrade.Quotes.Finam (
downloadFinamSymbols,
Symbol(..),
Period(..),
DateFormat(..),
TimeFormat(..),
FieldSeparator(..),
RequestParams(..),
defaultParams,
downloadQuotes,
parseQuotes,
downloadAndParseQuotes,
Row(..)
) where
import ATrade.Types
import Control.Error.Util
import Control.Exception
import Control.Lens
import Control.Monad
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as BL
import Data.Csv hiding (Options)
import Data.List
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.ICU.Convert as TC
import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.Format
import qualified Data.Vector as V
import Network.Wreq
import Safe
import System.Log.Logger
import Text.Parsec
import Text.ParserCombinators.Parsec.Number
data Period =
PeriodTick |
Period1Min |
Period5Min |
Period10Min |
Period15Min |
Period30Min |
PeriodHour |
PeriodDay |
PeriodWeek |
PeriodMonth
deriving (Show, Eq)
instance Enum Period where
fromEnum PeriodTick = 1
fromEnum Period1Min = 2
fromEnum Period5Min = 3
fromEnum Period10Min = 4
fromEnum Period15Min = 5
fromEnum Period30Min = 6
fromEnum PeriodHour = 7
fromEnum PeriodDay = 8
fromEnum PeriodWeek = 9
fromEnum PeriodMonth = 10
toEnum 1 = PeriodTick
toEnum 2 = Period1Min
toEnum 3 = Period5Min
toEnum 4 = Period10Min
toEnum 5 = Period15Min
toEnum 6 = Period30Min
toEnum 7 = PeriodHour
toEnum 8 = PeriodDay
toEnum 9 = PeriodWeek
toEnum 10 = PeriodMonth
toEnum _ = PeriodDay
data DateFormat =
FormatYYYYMMDD |
FormatYYMMDD |
FormatDDMMYY |
FormatDD_MM_YY |
FormatMM_DD_YY
deriving (Show, Eq)
instance Enum DateFormat where
fromEnum FormatYYYYMMDD = 1
fromEnum FormatYYMMDD = 2
fromEnum FormatDDMMYY = 3
fromEnum FormatDD_MM_YY = 4
fromEnum FormatMM_DD_YY = 5
toEnum 1 = FormatYYYYMMDD
toEnum 2 = FormatYYMMDD
toEnum 3 = FormatDDMMYY
toEnum 4 = FormatDD_MM_YY
toEnum 5 = FormatMM_DD_YY
toEnum _ = FormatYYYYMMDD
data TimeFormat =
FormatHHMMSS |
FormatHHMM |
FormatHH_MM_SS |
FormatHH_MM
deriving (Show, Eq)
instance Enum TimeFormat where
fromEnum FormatHHMMSS = 1
fromEnum FormatHHMM = 2
fromEnum FormatHH_MM_SS = 3
fromEnum FormatHH_MM = 4
toEnum 1 = FormatHHMMSS
toEnum 2 = FormatHHMM
toEnum 3 = FormatHH_MM_SS
toEnum 4 = FormatHH_MM
toEnum _ = FormatHHMMSS
data FieldSeparator =
SeparatorComma |
SeparatorPeriod |
SeparatorSemicolon |
SeparatorTab |
SeparatorSpace
deriving (Show, Eq)
instance Enum FieldSeparator where
fromEnum SeparatorComma = 1
fromEnum SeparatorPeriod = 2
fromEnum SeparatorSemicolon = 3
fromEnum SeparatorTab = 4
fromEnum SeparatorSpace = 5
toEnum 1 = SeparatorComma
toEnum 2 = SeparatorPeriod
toEnum 3 = SeparatorSemicolon
toEnum 4 = SeparatorTab
toEnum 5 = SeparatorSpace
toEnum _ = SeparatorComma
data RequestParams = RequestParams {
ticker :: T.Text,
startDate :: Day,
endDate :: Day,
period :: Period,
dateFormat :: DateFormat,
timeFormat :: TimeFormat,
fieldSeparator :: FieldSeparator,
includeHeader :: Bool,
fillEmpty :: Bool
}
defaultParams :: RequestParams
defaultParams = RequestParams {
ticker = "",
startDate = fromGregorian 1970 1 1,
endDate = fromGregorian 1970 1 1,
period = PeriodDay,
dateFormat = FormatYYYYMMDD,
timeFormat = FormatHHMMSS,
fieldSeparator = SeparatorComma,
includeHeader = True,
fillEmpty = False
}
data Symbol = Symbol {
symCode :: T.Text,
symName :: T.Text,
symId :: Integer,
symMarketCode :: Integer,
symMarketName :: T.Text
}
deriving (Show, Eq)
data Row = Row {
rowTicker :: T.Text,
rowTime :: UTCTime,
rowOpen :: Price,
rowHigh :: Price,
rowLow :: Price,
rowClose :: Price,
rowVolume :: Integer
} deriving (Show, Eq)
instance FromField Price where
parseField s = fromDouble <$> (parseField s :: Parser Double)
instance FromRecord Row where
parseRecord v
| length v == 9 = do
tkr <- v .! 0
date <- v .! 2
time <- v .! 3
dt <- addUTCTime (-3 * 3600) <$> (parseDt date time)
open <- v .! 4
high <- v .! 5
low <- v .! 6
close <- v .! 7
vol <- v .! 8
return $ Row tkr dt open high low close vol
| otherwise = mzero
where
parseDt :: B.ByteString -> B.ByteString -> Parser UTCTime
parseDt d t = case parseTimeM True defaultTimeLocale "%Y%m%d %H%M%S" $ B8.unpack d ++ " " ++ B8.unpack t of
Just dt -> return dt
Nothing -> fail "Unable to parse date/time"
downloadAndParseQuotes :: RequestParams -> IO (Maybe [Row])
downloadAndParseQuotes requestParams = downloadAndParseQuotes' 3
where
downloadAndParseQuotes' iter = do
raw <- downloadQuotes requestParams `catch` (\e -> do
debugM "History" $ "exception: " ++ show (e :: SomeException)
return Nothing)
case raw of
Just r -> return $ parseQuotes r
Nothing -> if iter <= 0 then return Nothing else downloadAndParseQuotes' (iter - 1)
parseQuotes :: B.ByteString -> Maybe [Row]
parseQuotes csvData = case decode HasHeader $ BL.fromStrict csvData of
Left _ -> Nothing
Right d -> Just $ V.toList d
downloadQuotes :: RequestParams -> IO (Maybe B.ByteString)
downloadQuotes requestParams = do
symbols <- downloadFinamSymbols
case requestUrl symbols requestParams of
Just (url, options') -> do
resp <- getWith options' url
return $ Just $ BL.toStrict $ resp ^. responseBody
Nothing -> return Nothing
requestUrl :: [Symbol] -> RequestParams -> Maybe (String, Options)
requestUrl symbols requestParams = case getFinamCode symbols (ticker requestParams) of
Just (sym, market) -> Just ("http://export.finam.ru/export9.out", getOptions sym market)
Nothing -> Nothing
where
getOptions sym market = defaults &
param "market" .~ [T.pack . show $ market] &
param "f" .~ [ticker requestParams] &
param "e" .~ [".csv"] &
param "dtf" .~ [T.pack . show . fromEnum . dateFormat $ requestParams] &
param "tmf" .~ [T.pack . show . fromEnum . dateFormat $ requestParams] &
param "MSOR" .~ ["0"] &
param "mstime" .~ ["on"] &
param "mstimever" .~ ["1"] &
param "sep" .~ [T.pack . show . fromEnum . fieldSeparator $ requestParams] &
param "sep2" .~ ["1"] &
param "at" .~ [if includeHeader requestParams then "1" else "0"] &
param "fsp" .~ [if fillEmpty requestParams then "1" else "0"] &
param "p" .~ [T.pack . show . fromEnum $ period requestParams] &
param "em" .~ [T.pack . show $ sym ] &
param "df" .~ [T.pack . show $ dayFrom] &
param "mf" .~ [T.pack . show $ (monthFrom - 1)] &
param "yf" .~ [T.pack . show $ yearFrom] &
param "dt" .~ [T.pack . show $ dayTo] &
param "mt" .~ [T.pack . show $ (monthTo - 1)] &
param "yt" .~ [T.pack . show $ yearTo] &
param "code" .~ [ticker requestParams] &
param "datf" .~ if period requestParams == PeriodTick then ["11"] else ["1"]
(yearFrom, monthFrom, dayFrom) = toGregorian $ startDate requestParams
(yearTo, monthTo, dayTo) = toGregorian $ endDate requestParams
getFinamCode :: [Symbol] -> T.Text -> Maybe (Integer, Integer)
getFinamCode symbols tickerCode = case find (\x -> symCode x == tickerCode && symMarketCode x `notElem` archives) symbols of
Just sym -> Just (symId sym, symMarketCode sym)
Nothing -> Nothing
downloadFinamSymbols :: IO [Symbol]
downloadFinamSymbols = do
conv <- TC.open "cp1251" Nothing
result <- get "http://www.finam.ru/cache/icharts/icharts.js"
if result ^. responseStatus . statusCode == 200
then return $ parseSymbols . T.lines $ TC.toUnicode conv $ BL.toStrict $ result ^. responseBody
else return []
where
parseSymbols :: [T.Text] -> [Symbol]
parseSymbols strs = zipWith5 Symbol codes names ids marketCodes marketNames
where
getWithParser parser pos = fromMaybe [] $ do
s <- T.unpack <$> strs `atMay` pos
hush $ parse parser "" s
ids :: [Integer]
ids = getWithParser intlist 0
names :: [T.Text]
names = T.pack <$> getWithParser strlist 1
codes :: [T.Text]
codes = T.pack <$> getWithParser strlist 2
marketCodes :: [Integer]
marketCodes = getWithParser intlist 3
marketNames :: [T.Text]
marketNames = fmap (\code -> fromMaybe "" $ M.lookup code codeToName) marketCodes
intlist = do
_ <- string "var"
spaces
skipMany1 alphaNum
spaces
_ <- char '='
spaces
_ <- char '['
manyTill (do
i <- int
_ <- char ',' <|> char ']'
return i) (char '\'' <|> char ';')
strlist = do
_ <- string "var"
spaces
skipMany1 alphaNum
spaces
_ <- char '='
spaces
_ <- char '['
(char '\'' >> manyTill ((char '\\' >> char '\'') <|> anyChar) (char '\'')) `sepBy` char ','
codeToName :: M.Map Integer T.Text
codeToName = M.fromList [
(200, "МосБиржа топ"),
(1 , "МосБиржа акции"),
(14 , "МосБиржа фьючерсы"),
(41, "Курс рубля"),
(45, "МосБиржа валютный рынок"),
(2, "МосБиржа облигации"),
(12, "МосБиржа внесписочные облигации"),
(29, "МосБиржа пифы"),
(8, "Расписки"),
(6, "Мировые Индексы"),
(24, "Товары"),
(5, "Мировые валюты"),
(25, "Акции США(BATS)"),
(7, "Фьючерсы США"),
(27, "Отрасли экономики США"),
(26, "Гособлигации США"),
(28, "ETF"),
(30, "Индексы мировой экономики"),
(3, "РТС"),
(20, "RTS Board"),
(10, "РТС-GAZ"),
(17, "ФОРТС Архив"),
(31, "Сырье Архив"),
(38, "RTS Standard Архив"),
(16, "ММВБ Архив"),
(18, "РТС Архив"),
(9, "СПФБ Архив"),
(32, "РТС-BOARD Архив"),
(39, "Расписки Архив"),
(-1, "Отрасли") ]
archives :: [Integer]
archives = [3, 8, 16, 17, 18, 31, 32, 38, 39, 517]

12
src/ATrade/Quotes/HistoryProvider.hs

@ -1,12 +0,0 @@
module ATrade.Quotes.HistoryProvider
(
HistoryProvider(..)
) where
import ATrade.RoboCom.Types (Bar)
import ATrade.Types (BarTimeframe, TickerId)
import Data.Time (UTCTime)
class (Monad m) => HistoryProvider m where
getHistory :: TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar]

21
src/ATrade/Quotes/QHP.hs

@ -1,6 +1,4 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module ATrade.Quotes.QHP ( module ATrade.Quotes.QHP (
Period(..), Period(..),
@ -11,9 +9,7 @@ module ATrade.Quotes.QHP (
) where ) where
import ATrade.Exceptions import ATrade.Exceptions
import ATrade.Logging (Message, logInfo, logDebug)
import ATrade.Types import ATrade.Types
import Colog (WithLog)
import Control.Exception.Safe (MonadThrow, throw) import Control.Exception.Safe (MonadThrow, throw)
import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson import Data.Aeson
@ -24,7 +20,7 @@ import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Data.Time.Format import Data.Time.Format
import Language.Haskell.Printf (t) import System.Log.Logger
import System.ZMQ4 import System.ZMQ4
data Period = data Period =
@ -57,10 +53,10 @@ data QHPHandle = QHPHandle
mkQHPHandle :: Context -> T.Text -> QHPHandle mkQHPHandle :: Context -> T.Text -> QHPHandle
mkQHPHandle = QHPHandle mkQHPHandle = QHPHandle
requestHistoryFromQHP :: (WithLog env Message m, MonadThrow m, MonadIO m) => QHPHandle -> TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar] requestHistoryFromQHP :: (MonadThrow m, MonadIO m) => QHPHandle -> TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar]
requestHistoryFromQHP qhp tickerId timeframe fromTime toTime = requestHistoryFromQHP qhp tickerId timeframe fromTime toTime =
case parseQHPPeriod (unBarTimeframe timeframe) of case parseQHPPeriod (unBarTimeframe timeframe) of
Just tf -> getQuotes (qhpContext qhp) (params tf) Just tf -> liftIO $ getQuotes (qhpContext qhp) (params tf)
_ -> throw $ BadParams "QHP: Unable to parse timeframe" _ -> throw $ BadParams "QHP: Unable to parse timeframe"
where where
params tf = RequestParams params tf = RequestParams
@ -100,11 +96,10 @@ instance ToJSON RequestParams where
"to" .= printDatetime (UTCTime (endDate p) 0), "to" .= printDatetime (UTCTime (endDate p) 0),
"timeframe" .= show (period p) ] "timeframe" .= show (period p) ]
getQuotes :: (WithLog env Message m, MonadIO m) => Context -> RequestParams -> m [Bar] getQuotes :: Context -> RequestParams -> IO [Bar]
getQuotes ctx params = do getQuotes ctx params =
logInfo "QHP" $ "Connecting to ep: " <> endpoint params withSocket ctx Req $ \sock -> do
logDebug "QHP" $ "From: " <> (T.pack . show) (startDate params) <> "; To: " <> (T.pack . show) (endDate params) debugM "QHP" $ "Connecting to ep: " ++ show (endpoint params)
result <- liftIO $ withSocket ctx Req $ \sock -> do
connect sock $ (T.unpack . endpoint) params connect sock $ (T.unpack . endpoint) params
send sock [] (BL.toStrict $ encode params) send sock [] (BL.toStrict $ encode params)
response <- receiveMulti sock response <- receiveMulti sock
@ -113,8 +108,6 @@ getQuotes ctx params = do
then return $ reverse $ parseBars (ticker params) $ BL.fromStrict rest then return $ reverse $ parseBars (ticker params) $ BL.fromStrict rest
else return [] else return []
_ -> return [] _ -> return []
logInfo "QHP" $ "Obtained bars: " <> (T.pack . show . length) result
return result
parseBars :: TickerId -> BL.ByteString -> [Bar] parseBars :: TickerId -> BL.ByteString -> [Bar]
parseBars tickerId input = parseBars tickerId input =

14
src/ATrade/Quotes/QTIS.hs

@ -1,4 +1,3 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module ATrade.Quotes.QTIS module ATrade.Quotes.QTIS
@ -8,15 +7,13 @@ module ATrade.Quotes.QTIS
) where ) where
import ATrade.Exceptions import ATrade.Exceptions
import ATrade.Logging (Message, logInfo)
import ATrade.Types import ATrade.Types
import Colog (WithLog)
import Control.Exception.Safe import Control.Exception.Safe
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Aeson import Data.Aeson
import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Char8 as BC8
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T import qualified Data.Text as T
import System.Log.Logger
import System.ZMQ4 import System.ZMQ4
data TickerInfo = TickerInfo { data TickerInfo = TickerInfo {
@ -37,13 +34,16 @@ instance ToJSON TickerInfo where
"lot_size" .= tiLotSize ti, "lot_size" .= tiLotSize ti,
"tick_size" .= tiTickSize ti ] "tick_size" .= tiTickSize ti ]
qtisGetTickersInfo :: (MonadIO m) => Context -> T.Text -> TickerId -> m TickerInfo qtisGetTickersInfo :: Context -> T.Text -> TickerId -> IO TickerInfo
qtisGetTickersInfo ctx endpoint tickerId = do qtisGetTickersInfo ctx endpoint tickerId =
liftIO $ withSocket ctx Req $ \sock -> do withSocket ctx Req $ \sock -> do
debugM "QTIS" $ "Connecting to: " ++ T.unpack endpoint
connect sock $ T.unpack endpoint connect sock $ T.unpack endpoint
debugM "QTIS" $ "Requesting: " ++ T.unpack tickerId
send sock [] $ BL.toStrict tickerRequest send sock [] $ BL.toStrict tickerRequest
response <- receiveMulti sock response <- receiveMulti sock
let r = parseResponse response let r = parseResponse response
debugM "QTIS" $ "Got response: " ++ show r
case r of case r of
Just resp -> return resp Just resp -> return resp
Nothing -> throw $ QTISFailure "Can't parse response" Nothing -> throw $ QTISFailure "Can't parse response"

12
src/ATrade/Quotes/TickerInfoProvider.hs

@ -1,12 +0,0 @@
module ATrade.Quotes.TickerInfoProvider
(
TickerInfoProvider(..)
) where
import ATrade.RoboCom.Types (InstrumentParameters)
import ATrade.Types (TickerId)
class (Monad m) => TickerInfoProvider m where
getInstrumentParameters :: [TickerId] -> m [InstrumentParameters]

0
src/ATrade/Quotes/Types.hs

14
src/ATrade/RoboCom/ConfigStorage.hs

@ -1,14 +0,0 @@
{-# LANGUAGE RankNTypes #-}
module ATrade.RoboCom.ConfigStorage
(
ConfigStorage(..)
) where
import qualified Data.Text as T
import Dhall (FromDhall)
class (Monad m) => ConfigStorage m where
loadConfig :: forall c. (FromDhall c) => T.Text -> m c

32
src/ATrade/RoboCom/Monad.hs

@ -13,16 +13,15 @@ module ATrade.RoboCom.Monad (
seInstanceId, seInstanceId,
seAccount, seAccount,
seVolume, seVolume,
seBars,
seLastTimestamp, seLastTimestamp,
EventCallback, EventCallback,
Event(..), Event(..),
MonadRobot(..), MonadRobot(..),
also, also,
t, t,
st, st
getFirstTickerId, ) where
getTickerAnyTimeframe
) where
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
@ -32,17 +31,13 @@ import Data.Aeson.Types
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Data.Time.Clock import Data.Time.Clock
import qualified Data.List as L
import Language.Haskell.Printf import Language.Haskell.Printf
import Language.Haskell.TH.Quote (QuasiQuoter) import Language.Haskell.TH.Quote (QuasiQuoter)
import ATrade.Logging (Severity)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
class (Monad m) => MonadRobot m c s | m -> c, m -> s where class (Monad m) => MonadRobot m c s | m -> c, m -> s where
submitOrder :: Order -> m OrderId submitOrder :: Order -> m ()
cancelOrder :: OrderId -> m () cancelOrder :: OrderId -> m ()
appendToLog :: Severity -> TL.Text -> m () appendToLog :: TL.Text -> m ()
setupTimer :: UTCTime -> m () setupTimer :: UTCTime -> m ()
enqueueIOAction :: Int -> IO Value -> m () enqueueIOAction :: Int -> IO Value -> m ()
getConfig :: m c getConfig :: m c
@ -53,27 +48,13 @@ class (Monad m) => MonadRobot m c s | m -> c, m -> s where
oldState <- getState oldState <- getState
setState (f oldState) setState (f oldState)
getEnvironment :: m StrategyEnvironment getEnvironment :: m StrategyEnvironment
getTicker :: TickerId -> BarTimeframe -> m (Maybe BarSeries)
getTickerInfo :: TickerId -> m (Maybe InstrumentParameters)
getAvailableTickers :: m (NonEmpty BarSeriesId)
getFirstTickerId :: forall c s m. (Monad m, MonadRobot m c s) => m BarSeriesId
getFirstTickerId = NE.head <$> getAvailableTickers
getTickerAnyTimeframe :: forall c s m. (Monad m, MonadRobot m c s) => TickerId -> m (Maybe BarSeries)
getTickerAnyTimeframe requestedTickerId = do
tickers <- getAvailableTickers
case L.find (\(BarSeriesId tid _) -> tid == requestedTickerId) tickers of
Just (BarSeriesId tid tf) -> getTicker tid tf
Nothing -> return Nothing
st :: QuasiQuoter st :: QuasiQuoter
st = t st = t
type EventCallback c s = forall m . MonadRobot m c s => Event -> m () type EventCallback c s = forall m . MonadRobot m c s => Event -> m ()
data Event = NewBar (BarTimeframe, Bar) data Event = NewBar Bar
| NewTick Tick | NewTick Tick
| OrderSubmitted Order | OrderSubmitted Order
| OrderUpdate OrderId OrderState | OrderUpdate OrderId OrderState
@ -87,6 +68,7 @@ data StrategyEnvironment = StrategyEnvironment {
_seInstanceId :: !T.Text, -- ^ Strategy instance identifier. Should be unique among all strategies (very desirable) _seInstanceId :: !T.Text, -- ^ Strategy instance identifier. Should be unique among all strategies (very desirable)
_seAccount :: !T.Text, -- ^ Account string to use for this strategy instance. Broker-dependent _seAccount :: !T.Text, -- ^ Account string to use for this strategy instance. Broker-dependent
_seVolume :: !Int, -- ^ Volume to use for this instance (in lots/contracts) _seVolume :: !Int, -- ^ Volume to use for this instance (in lots/contracts)
_seBars :: !Bars, -- ^ List of tickers which is used by this strategy
_seLastTimestamp :: !UTCTime _seLastTimestamp :: !UTCTime
} deriving (Eq) } deriving (Eq)
makeLenses ''StrategyEnvironment makeLenses ''StrategyEnvironment

16
src/ATrade/RoboCom/Persistence.hs

@ -1,16 +0,0 @@
{-# LANGUAGE RankNTypes #-}
module ATrade.RoboCom.Persistence
(
MonadPersistence(..)
) where
import Data.Aeson
import Data.Default (Default)
import qualified Data.Text as T
class (Monad m) => MonadPersistence m where
saveState :: forall s. (ToJSON s) => s -> T.Text -> m ()
loadState :: forall s. (Default s, FromJSON s) => T.Text -> m s

296
src/ATrade/RoboCom/Positions.hs

@ -1,4 +1,3 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
@ -9,20 +8,19 @@
{-| {-|
- Module : ATrade.RoboCom.Combinators - Module : ATrade.RoboCom.Combinators
- Description : Reusable behavioural components of strategies - Description : Reusable behavioural components of strategies
- Copyright : (c) Denis Tereshkin 2021 - Copyright : (c) Denis Tereshkin 2016
- License : BSD 3-clause - License : Proprietary
- Maintainer : denis@kasan.ws - Maintainer : denis@kasan.ws
- Stability : experimental - Stability : experimental
- Portability : POSIX - Portability : POSIX
- -
- A lot of behaviour is common for most of the strategies. - A lot of behaviour is common for most of the strategies. This module contains those common blocks which can be composed to avoid boilerplate in main strategy code.
- This module contains those common blocks which can be composed to avoid boilerplate in main strategy code.
-} -}
module ATrade.RoboCom.Positions module ATrade.RoboCom.Positions
( (
StateHasPositions(..), StateHasPositions(..),
ParamsSize(..), ParamsHasMainTicker(..),
PositionState(..), PositionState(..),
Position(..), Position(..),
posIsOpen, posIsOpen,
@ -48,10 +46,12 @@ module ATrade.RoboCom.Positions
onTradeEvent, onTradeEvent,
onActionCompletedEvent, onActionCompletedEvent,
enterAtMarket, enterAtMarket,
enterAtMarketForTicker,
enterAtMarketWithParams, enterAtMarketWithParams,
enterAtLimit, enterAtLimit,
enterAtLimitWithVolume,
enterAtLimitWithParams,
enterAtLimitForTicker, enterAtLimitForTicker,
enterAtLimitForTickerWithVolume,
enterAtLimitForTickerWithParams, enterAtLimitForTickerWithParams,
enterLongAtMarket, enterLongAtMarket,
enterShortAtMarket, enterShortAtMarket,
@ -65,15 +65,8 @@ module ATrade.RoboCom.Positions
setStopLoss, setStopLoss,
setLimitStopLoss, setLimitStopLoss,
setTakeProfit, setTakeProfit,
setStopLossAndTakeProfit, setStopLossAndTakeProfit
) where
handlePositions,
calculateSizeIVS,
calculateSizeIVSWith,
calculateSizeFixed,
calculateSizeFixedCash,
calculateSizeFixedCashWith,
calculateSizeIVSWithMinimum) where
import GHC.Generics import GHC.Generics
@ -81,18 +74,15 @@ import ATrade.RoboCom.Monad
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Control.Lens hiding (op) import Control.Lens
import Control.Monad import Control.Monad
import ATrade.Logging (Severity (Trace, Warning))
import qualified ATrade.RoboCom.Indicators as I
import Data.Aeson import Data.Aeson
import qualified Data.List as L import qualified Data.List as L
import qualified Data.List.NonEmpty as NE import qualified Data.Map as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Data.Time.Clock import Data.Time.Clock
import GHC.Records (HasField (..))
data PositionState = PositionWaitingOpenSubmission Order data PositionState = PositionWaitingOpenSubmission Order
| PositionWaitingOpen | PositionWaitingOpen
@ -154,44 +144,8 @@ modifyPositions f = do
pos <- getPositions <$> getState pos <- getPositions <$> getState
modifyState (\s -> setPositions s (f pos)) modifyState (\s -> setPositions s (f pos))
class ParamsSize a where class ParamsHasMainTicker a where
getPositionSize :: a -> BarSeries -> Operation -> Int mainTicker :: a -> TickerId
calculateSizeIVS :: (HasField "riskSize" a Double,
HasField "stopSize" a Double,
HasField "atrPeriod" a Int) =>
a -> BarSeries -> Operation -> Int
calculateSizeIVS cfg = calculateSizeIVSWith (getField @"atrPeriod" cfg) (getField @"riskSize" cfg) (getField @"stopSize" cfg) cfg
calculateSizeIVSWithMinimum :: (HasField "riskSize" a Double,
HasField "stopSize" a Double,
HasField "atrPeriod" a Int) =>
Int -> a -> BarSeries -> Operation -> Int
calculateSizeIVSWithMinimum minVolume cfg series op = max (calculateSizeIVS cfg series op) minVolume
calculateSizeIVSWith :: Int -> Double -> Double -> a -> BarSeries -> Operation -> Int
calculateSizeIVSWith atrPeriod riskSize stopSize _ series _ =
let atr = I.atr atrPeriod (bsBars series) in
truncate (riskSize / (atr * stopSize))
calculateSizeFixed :: (HasField "positionSize" a Int) =>
a -> BarSeries -> Operation -> Int
calculateSizeFixed cfg _ _ = getField @"positionSize" cfg
calculateSizeFixedCash :: ( HasField "totalCash" a Double,
HasField "maxPositions" a Int) =>
a -> BarSeries -> Operation -> Int
calculateSizeFixedCash cfg = calculateSizeFixedCashWith (getField @"totalCash" cfg) (getField @"maxPositions" cfg) cfg
calculateSizeFixedCashWith :: Double -> Int -> a -> BarSeries -> Operation -> Int
calculateSizeFixedCashWith totalCash maxPositions cfg series _ =
case bsBars $ series of
(lastBar:_) ->
let cashPerPosition = totalCash / fromIntegral maxPositions in
truncate (cashPerPosition / ((toDouble $ barClose lastBar) * (fromIntegral $ ipLotSize . bsParams $ series)))
_ -> 0
-- | Helper function. Finds first element in list which satisfies predicate 'p' and if found, applies 'm' to it, leaving other elements intact. -- | Helper function. Finds first element in list which satisfies predicate 'p' and if found, applies 'm' to it, leaving other elements intact.
findAndModify :: (a -> Bool) -> (a -> a) -> [a] -> [a] findAndModify :: (a -> Bool) -> (a -> a) -> [a] -> [a]
@ -223,8 +177,7 @@ orderDeadline maybeDeadline lastTs =
dispatchPosition :: (StateHasPositions s, MonadRobot m c s) => Event -> Position -> m Position dispatchPosition :: (StateHasPositions s, MonadRobot m c s) => Event -> Position -> m Position
dispatchPosition event pos = dispatchPosition event pos = case posState pos of
case posState pos of
PositionWaitingOpenSubmission pendingOrder -> handlePositionWaitingOpenSubmission pendingOrder PositionWaitingOpenSubmission pendingOrder -> handlePositionWaitingOpenSubmission pendingOrder
PositionWaitingOpen -> handlePositionWaitingOpen PositionWaitingOpen -> handlePositionWaitingOpen
PositionOpen -> handlePositionOpen PositionOpen -> handlePositionOpen
@ -237,13 +190,11 @@ dispatchPosition event pos =
handlePositionWaitingOpenSubmission pendingOrder = do handlePositionWaitingOpenSubmission pendingOrder = do
lastTs <- view seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if orderDeadline (posSubmissionDeadline pos) lastTs if orderDeadline (posSubmissionDeadline pos) lastTs
then do then return $ pos { posState = PositionCancelled } -- TODO call TimeoutHandler if present
appendToLog Warning $ [t|Submission deadline: %?, %?|] lastTs (posSubmissionDeadline pos)
return $ pos { posState = PositionCancelled } -- TODO call TimeoutHandler if present
else case event of else case event of
OrderUpdate oid Submitted -> do OrderSubmitted order ->
return $ if orderId pendingOrder == oid return $ if order `orderCorrespondsTo` pendingOrder
then pos { posCurrentOrder = Just pendingOrder, then pos { posCurrentOrder = Just order,
posState = PositionWaitingOpen, posState = PositionWaitingOpen,
posSubmissionDeadline = Nothing } posSubmissionDeadline = Nothing }
else pos else pos
@ -256,52 +207,49 @@ dispatchPosition event pos =
then then
if posBalance pos == 0 if posBalance pos == 0
then do then do
appendToLog $ [t|"In PositionWaitingOpen: execution timeout: %?/%?"|] (posExecutionDeadline pos) lastTs
cancelOrder $ orderId order cancelOrder $ orderId order
return $ pos { posState = PositionWaitingPendingCancellation, posNextState = Just PositionCancelled } return $ pos { posState = PositionWaitingPendingCancellation, posNextState = Just PositionCancelled }
else do else do
appendToLog Trace $ [t|Order executed (partially, %? / %?): %?|] (posBalance pos) (orderQuantity order) order appendToLog $ [t|Order executed (partially, %? / %?): %?|] (posBalance pos) (orderQuantity order) order
return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posEntryTime = Just lastTs} return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posEntryTime = Just lastTs}
else case event of else case event of
OrderUpdate oid newstate -> OrderUpdate oid newstate ->
if oid == orderId order if oid == orderId order
then case newstate of then case newstate of
Cancelled -> do Cancelled -> do
appendToLog Trace $ [t|Order cancelled in PositionWaitingOpen: balance %d, max %d|] (posBalance pos) (orderQuantity order) appendToLog $ [t|Order cancelled in PositionWaitingOpen: balance %d, max %d|] (posBalance pos) (orderQuantity order)
if posBalance pos /= 0 if posBalance pos /= 0
then return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posEntryTime = Just lastTs} then return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posEntryTime = Just lastTs}
else return pos { posState = PositionCancelled } else return pos { posState = PositionCancelled }
Executed -> do Executed -> do
appendToLog Trace $ [t|Order executed: %?|] order appendToLog $ [t|Order executed: %?|] order
return pos { posState = PositionOpen, return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posBalance = balanceForOrder order, posEntryTime = Just lastTs}
posCurrentOrder = Nothing,
posExecutionDeadline = Nothing,
posBalance = balanceForOrder order,
posEntryTime = Just lastTs }
Rejected -> do Rejected -> do
appendToLog Trace $ [t|Order rejected: %?|] order appendToLog $ [t|Order rejected: %?|] order
return pos { posState = PositionCancelled, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posBalance = 0, posEntryTime = Nothing } return pos { posState = PositionCancelled, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posBalance = 0, posEntryTime = Nothing }
_ -> do _ -> do
appendToLog Trace $ [t|In PositionWaitingOpen: order state update: %?|] newstate appendToLog $ [t|In PositionWaitingOpen: order state update: %?|] newstate
return pos return pos
else return pos -- Update for another position's order else return pos -- Update for another position's order
NewTrade trade -> do NewTrade trade -> do
appendToLog Trace $ [t|Order new trade: %?/%?|] order trade appendToLog $ [t|Order new trade: %?/%?|] order trade
return $ if tradeOrderId trade == orderId order return $ if tradeOrderId trade == orderId order
then pos { posBalance = if tradeOperation trade == Buy then posBalance pos + tradeQuantity trade else posBalance pos - tradeQuantity trade } then pos { posBalance = if tradeOperation trade == Buy then posBalance pos + tradeQuantity trade else posBalance pos - tradeQuantity trade }
else pos else pos
_ -> return pos _ -> return pos
Nothing -> do Nothing -> do
appendToLog Warning $ [t|W: No current order in PositionWaitingOpen state: %?|] pos appendToLog $ [t|W: No current order in PositionWaitingOpen state: %?|] pos
return pos return pos
handlePositionOpen = do handlePositionOpen = do
lastTs <- view seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if if
| orderDeadline (posSubmissionDeadline pos) lastTs -> do | orderDeadline (posSubmissionDeadline pos) lastTs -> do
appendToLog Warning $ [t|PositionId: %? : Missed submission deadline: %?, remaining in PositionOpen state|] (posId pos) (posSubmissionDeadline pos) appendToLog $ [t|PositionId: %? : Missed submission deadline: %?, remaining in PositionOpen state|] (posId pos) (posSubmissionDeadline pos)
return pos { posSubmissionDeadline = Nothing, posExecutionDeadline = Nothing } return pos { posSubmissionDeadline = Nothing, posExecutionDeadline = Nothing }
| orderDeadline (posExecutionDeadline pos) lastTs -> do | orderDeadline (posExecutionDeadline pos) lastTs -> do
appendToLog Warning $ [t|PositionId: %? : Missed execution deadline: %?, remaining in PositionOpen state|] (posId pos) (posExecutionDeadline pos) appendToLog $ [t|PositionId: %? : Missed execution deadline: %?, remaining in PositionOpen state|] (posId pos) (posExecutionDeadline pos)
return pos { posExecutionDeadline = Nothing } return pos { posExecutionDeadline = Nothing }
| otherwise -> case event of | otherwise -> case event of
NewTick tick -> if NewTick tick -> if
@ -324,11 +272,8 @@ dispatchPosition event pos =
(OrderUpdate _ newstate, Just _, Just (PositionWaitingCloseSubmission nextOrder)) -> (OrderUpdate _ newstate, Just _, Just (PositionWaitingCloseSubmission nextOrder)) ->
if newstate == Cancelled if newstate == Cancelled
then do then do
oid <- submitOrder nextOrder submitOrder nextOrder
return pos return pos { posState = PositionWaitingCloseSubmission nextOrder, posSubmissionDeadline = Just (10 `addUTCTime` lastTs), posExecutionDeadline = Nothing }
{ posState = PositionWaitingCloseSubmission nextOrder { orderId = oid },
posSubmissionDeadline = Just (10 `addUTCTime` lastTs),
posExecutionDeadline = Nothing }
else return pos else return pos
(OrderUpdate _ newstate, Just _, Just PositionCancelled) -> (OrderUpdate _ newstate, Just _, Just PositionCancelled) ->
if newstate == Cancelled if newstate == Cancelled
@ -336,7 +281,7 @@ dispatchPosition event pos =
else return pos else return pos
_ -> return pos _ -> return pos
else do else do
appendToLog Warning "Deadline when cancelling pending order" appendToLog "Deadline when cancelling pending order"
return pos { posState = PositionCancelled } return pos { posState = PositionCancelled }
handlePositionWaitingCloseSubmission pendingOrder = do handlePositionWaitingCloseSubmission pendingOrder = do
@ -348,9 +293,9 @@ dispatchPosition event pos =
Nothing -> doNothing Nothing -> doNothing
return $ pos { posCurrentOrder = Nothing, posState = PositionOpen, posSubmissionDeadline = Nothing } -- TODO call TimeoutHandler if present return $ pos { posCurrentOrder = Nothing, posState = PositionOpen, posSubmissionDeadline = Nothing } -- TODO call TimeoutHandler if present
else case event of else case event of
OrderUpdate oid Submitted -> OrderSubmitted order ->
return $ if orderId pendingOrder == oid return $ if order `orderCorrespondsTo` pendingOrder
then pos { posCurrentOrder = Just pendingOrder, then pos { posCurrentOrder = Just order,
posState = PositionWaitingClose, posState = PositionWaitingClose,
posSubmissionDeadline = Nothing } posSubmissionDeadline = Nothing }
else pos else pos
@ -363,7 +308,7 @@ dispatchPosition event pos =
case posCurrentOrder pos of case posCurrentOrder pos of
Just order -> cancelOrder (orderId order) Just order -> cancelOrder (orderId order)
_ -> doNothing _ -> doNothing
appendToLog Warning $ [t|Was unable to close position, remaining balance: %?|] (posBalance pos) appendToLog $ [t|Was unable to close position, remaining balance: %?|] (posBalance pos)
return $ pos { posState = PositionOpen, posSubmissionDeadline = Nothing, posExecutionDeadline = Nothing } -- TODO call TimeoutHandler if present return $ pos { posState = PositionOpen, posSubmissionDeadline = Nothing, posExecutionDeadline = Nothing } -- TODO call TimeoutHandler if present
else case (event, posCurrentOrder pos) of else case (event, posCurrentOrder pos) of
(OrderUpdate oid newstate, Just order) -> (OrderUpdate oid newstate, Just order) ->
@ -419,29 +364,14 @@ newPosition order account tickerId operation quantity submissionDeadline = do
posExitTime = Nothing posExitTime = Nothing
} }
modifyPositions (\p -> position : p) modifyPositions (\p -> position : p)
positions <- getPositions <$> getState
appendToLog $ [t|All positions: %?|] positions
return position return position
rejectedPosition :: (StateHasPositions s, MonadRobot m c s) => m Position
rejectedPosition =
return Position {
posId = "Rejected",
posAccount = "",
posTicker = "",
posBalance = 0,
posState = PositionCancelled,
posNextState = Nothing,
posStopPrice = Nothing,
posStopLimitPrice = Nothing,
posTakeProfitPrice = Nothing,
posCurrentOrder = Nothing,
posSubmissionDeadline = Nothing,
posExecutionDeadline = Nothing,
posEntryTime = Nothing,
posExitTime = Nothing
}
reapDeadPositions :: (StateHasPositions s) => EventCallback c s reapDeadPositions :: (StateHasPositions s) => EventCallback c s
reapDeadPositions _ = modifyPositions (L.filter (not . posIsDead)) reapDeadPositions _ = do
ts <- view seLastTimestamp <$> getEnvironment
when (floor (utctDayTime ts) `mod` 300 == 0) $ modifyPositions (L.filter (not . posIsDead))
defaultHandler :: (StateHasPositions s) => EventCallback c s defaultHandler :: (StateHasPositions s) => EventCallback c s
defaultHandler = reapDeadPositions `also` handlePositions defaultHandler = reapDeadPositions `also` handlePositions
@ -456,18 +386,18 @@ modifyPosition f oldpos = do
return $ f oldpos return $ f oldpos
Nothing -> return oldpos Nothing -> return oldpos
getCurrentTicker :: (MonadRobot m c s) => m [Bar] getCurrentTicker :: (ParamsHasMainTicker c, MonadRobot m c s) => m [Bar]
getCurrentTicker = do getCurrentTicker = do
(BarSeriesId mainTicker' tf) <- NE.head <$> getAvailableTickers mainTicker' <- mainTicker <$> getConfig
maybeBars <- getTicker mainTicker' tf maybeBars <- view (seBars . at mainTicker') <$> getEnvironment
case maybeBars of case maybeBars of
Just b -> return $ bsBars b Just b -> return $ bsBars b
_ -> return [] _ -> return []
getCurrentTickerSeries :: (MonadRobot m c s) => m (Maybe BarSeries) getCurrentTickerSeries :: (ParamsHasMainTicker c, MonadRobot m c s) => m (Maybe BarSeries)
getCurrentTickerSeries = do getCurrentTickerSeries = do
(BarSeriesId mainTicker' tf) <- NE.head <$> getAvailableTickers bars <- view seBars <$> getEnvironment
getTicker mainTicker' tf flip M.lookup bars . mainTicker <$> getConfig
getLastActivePosition :: (StateHasPositions s, MonadRobot m c s) => m (Maybe Position) getLastActivePosition :: (StateHasPositions s, MonadRobot m c s) => m (Maybe Position)
getLastActivePosition = L.find (\pos -> posState pos == PositionOpen) . getPositions <$> getState getLastActivePosition = L.find (\pos -> posState pos == PositionOpen) . getPositions <$> getState
@ -488,7 +418,7 @@ getAllActiveAndPendingPositions = L.filter
onNewBarEvent :: (MonadRobot m c s) => Event -> (Bar -> m ()) -> m () onNewBarEvent :: (MonadRobot m c s) => Event -> (Bar -> m ()) -> m ()
onNewBarEvent event f = case event of onNewBarEvent event f = case event of
NewBar (_, bar) -> f bar NewBar bar -> f bar
_ -> doNothing _ -> doNothing
onNewTickEvent :: (MonadRobot m c s) => Event -> (Tick -> m ()) -> m () onNewTickEvent :: (MonadRobot m c s) => Event -> (Tick -> m ()) -> m ()
@ -527,31 +457,16 @@ onActionCompletedEvent event f = case event of
ActionCompleted tag v -> f tag v ActionCompleted tag v -> f tag v
_ -> doNothing _ -> doNothing
roundTo :: Price -> Price -> Price enterAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Operation -> m Position
roundTo quant v = quant * (fromIntegral . floor . toDouble) (v / quant)
enterAtMarket :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => T.Text -> Operation -> m Position
enterAtMarket operationSignalName operation = do enterAtMarket operationSignalName operation = do
bsId <- getFirstTickerId
enterAtMarketForTicker operationSignalName bsId operation
enterAtMarketForTicker :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => T.Text -> BarSeriesId -> Operation -> m Position
enterAtMarketForTicker operationSignalName (BarSeriesId tid tf) operation = do
maybeSeries <- getTicker tid tf
case maybeSeries of
Just series -> do
env <- getEnvironment env <- getEnvironment
cfg <- getConfig enterAtMarketWithParams (env ^. seAccount) (env ^. seVolume) (SignalId (env ^. seInstanceId) operationSignalName "") operation
let quantity = getPositionSize cfg series operation
enterAtMarketWithParams (env ^. seAccount) tid quantity (SignalId (env ^. seInstanceId) operationSignalName "") operation
Nothing -> do
appendToLog Warning $ "Unable to get ticker series: " <> TL.fromStrict tid
rejectedPosition
enterAtMarketWithParams :: (StateHasPositions s, MonadRobot m c s) => T.Text -> TickerId -> Int -> SignalId -> Operation -> m Position enterAtMarketWithParams :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Int -> SignalId -> Operation -> m Position
enterAtMarketWithParams account tid quantity signalId operation = do enterAtMarketWithParams account quantity signalId operation = do
oid <- submitOrder $ order tid tickerId <- mainTicker <$> getConfig
newPosition ((order tid) { orderId = oid }) account tid operation quantity 20 submitOrder $ order tickerId
newPosition (order tickerId) account tickerId operation quantity 20
where where
order tickerId = mkOrder { order tickerId = mkOrder {
orderAccountId = account, orderAccountId = account,
@ -562,41 +477,41 @@ enterAtMarketWithParams account tid quantity signalId operation = do
orderSignalId = signalId orderSignalId = signalId
} }
enterAtLimit :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => T.Text -> Price -> Operation -> m Position enterAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Price -> Operation -> m Position
enterAtLimit operationSignalName price operation = do enterAtLimit timeToCancel operationSignalName price operation = do
bsId <- getFirstTickerId
env <- getEnvironment env <- getEnvironment
enterAtLimitForTicker bsId operationSignalName price operation enterAtLimitWithParams timeToCancel (env ^. seAccount) (env ^. seVolume) (SignalId (env ^. seInstanceId) operationSignalName "") price operation
enterAtLimitWithVolume :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Price -> Int -> Operation -> m Position
enterAtLimitWithVolume timeToCancel operationSignalName price vol operation = do
acc <- view seAccount <$> getEnvironment
inst <- view seInstanceId <$> getEnvironment
enterAtLimitWithParams timeToCancel acc vol (SignalId inst operationSignalName "") price operation
enterAtLimitWithParams :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Int -> SignalId -> Price -> Operation -> m Position
enterAtLimitWithParams timeToCancel account quantity signalId price operation = do
tickerId <- mainTicker <$> getConfig
enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId price operation
enterAtLimitForTickerWithVolume :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Price -> Int -> Operation -> m Position
enterAtLimitForTickerWithVolume tickerId timeToCancel operationSignalName price vol operation = do
acc <- view seAccount <$> getEnvironment
inst <- view seInstanceId <$> getEnvironment
enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst operationSignalName "") price operation
enterAtLimitForTicker :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => BarSeriesId -> T.Text -> Price -> Operation -> m Position enterAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Price -> Operation -> m Position
enterAtLimitForTicker (BarSeriesId tid tf) operationSignalName price operation = do enterAtLimitForTicker tickerId timeToCancel operationSignalName price operation = do
acc <- view seAccount <$> getEnvironment acc <- view seAccount <$> getEnvironment
inst <- view seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
maybeSeries <- getTicker tid tf vol <- view seVolume <$> getEnvironment
case maybeSeries of enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst operationSignalName "") price operation
Just series -> do
cfg <- getConfig enterAtLimitForTickerWithParams :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Int -> SignalId -> Price -> Operation -> m Position
let quantity = getPositionSize cfg series operation
let roundedPrice = roundTo (ipTickSize . bsParams $ series) price
enterAtLimitForTickerWithParams tid (fromIntegral $ unBarTimeframe tf) acc quantity (SignalId inst operationSignalName "") roundedPrice operation
Nothing -> rejectedPosition
enterAtLimitForTickerWithParams ::
(StateHasPositions s,
MonadRobot m c s) =>
TickerId
-> NominalDiffTime
-> T.Text
-> Int
-> SignalId
-> Price
-> Operation
-> m Position
enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId price operation = do enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId price operation = do
lastTs <- view seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
oid <- submitOrder order submitOrder order
appendToLog Trace $ [t|enterAtLimit: %?, deadline: %?|] tickerId (timeToCancel `addUTCTime` lastTs) appendToLog $ [t|enterAtLimit: %?, deadline: %?|] tickerId (timeToCancel `addUTCTime` lastTs)
newPosition (order {orderId = oid}) account tickerId operation quantity 20 >>= newPosition order account tickerId operation quantity 20 >>=
modifyPosition (\p -> p { posExecutionDeadline = Just $ timeToCancel `addUTCTime` lastTs }) modifyPosition (\p -> p { posExecutionDeadline = Just $ timeToCancel `addUTCTime` lastTs })
where where
order = mkOrder { order = mkOrder {
@ -608,23 +523,23 @@ enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId
orderSignalId = signalId orderSignalId = signalId
} }
enterLongAtMarket :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => T.Text -> m Position enterLongAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position
enterLongAtMarket operationSignalName = enterAtMarket operationSignalName Buy enterLongAtMarket operationSignalName = enterAtMarket operationSignalName Buy
enterShortAtMarket :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => T.Text -> m Position enterShortAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position
enterShortAtMarket operationSignalName = enterAtMarket operationSignalName Sell enterShortAtMarket operationSignalName = enterAtMarket operationSignalName Sell
enterLongAtLimit :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => Price -> T.Text -> m Position enterLongAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position
enterLongAtLimit price operationSignalName = enterAtLimit operationSignalName price Buy enterLongAtLimit timeToCancel price operationSignalName = enterAtLimit timeToCancel operationSignalName price Buy
enterLongAtLimitForTicker :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => BarSeriesId -> Price -> T.Text -> m Position enterLongAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position
enterLongAtLimitForTicker tickerId price operationSignalName = enterAtLimitForTicker tickerId operationSignalName price Buy enterLongAtLimitForTicker tickerId timeToCancel price operationSignalName = enterAtLimitForTicker tickerId timeToCancel operationSignalName price Buy
enterShortAtLimit :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => Price -> T.Text -> m Position enterShortAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position
enterShortAtLimit price operationSignalName = enterAtLimit operationSignalName price Sell enterShortAtLimit timeToCancel price operationSignalName = enterAtLimit timeToCancel operationSignalName price Sell
enterShortAtLimitForTicker :: (StateHasPositions s, ParamsSize c, MonadRobot m c s) => BarSeriesId -> Price -> T.Text -> m Position enterShortAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position
enterShortAtLimitForTicker tickerId price operationSignalName = enterAtLimitForTicker tickerId operationSignalName price Sell enterShortAtLimitForTicker tickerId timeToCancel price operationSignalName = enterAtLimitForTicker tickerId timeToCancel operationSignalName price Sell
exitAtMarket :: (StateHasPositions s, MonadRobot m c s) => Position -> T.Text -> m Position exitAtMarket :: (StateHasPositions s, MonadRobot m c s) => Position -> T.Text -> m Position
exitAtMarket position operationSignalName = do exitAtMarket position operationSignalName = do
@ -640,10 +555,10 @@ exitAtMarket position operationSignalName = do
posExecutionDeadline = Nothing }) position posExecutionDeadline = Nothing }) position
Nothing -> do Nothing -> do
oid <- submitOrder (closeOrder inst) submitOrder (closeOrder inst)
modifyPosition (\pos -> modifyPosition (\pos ->
pos { posCurrentOrder = Nothing, pos { posCurrentOrder = Nothing,
posState = PositionWaitingCloseSubmission (closeOrder inst) { orderId = oid }, posState = PositionWaitingCloseSubmission (closeOrder inst),
posNextState = Just PositionClosed, posNextState = Just PositionClosed,
posSubmissionDeadline = Just $ 10 `addUTCTime` lastTs, posSubmissionDeadline = Just $ 10 `addUTCTime` lastTs,
posExecutionDeadline = Nothing }) position posExecutionDeadline = Nothing }) position
@ -661,32 +576,23 @@ exitAtLimit :: (StateHasPositions s, MonadRobot m c s) => NominalDiffTime -> Pri
exitAtLimit timeToCancel price position operationSignalName = do exitAtLimit timeToCancel price position operationSignalName = do
lastTs <- view seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
inst <- view seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
cfg <- getConfig
(BarSeriesId tid tf) <- getFirstTickerId
maybeSeries <- getTicker tid tf
case maybeSeries of
Just series -> do
let roundedPrice = roundTo (ipTickSize . bsParams $ series) price
case posCurrentOrder position of case posCurrentOrder position of
Just order -> cancelOrder (orderId order) Just order -> cancelOrder (orderId order)
Nothing -> doNothing Nothing -> doNothing
oid <- submitOrder (closeOrder inst roundedPrice) submitOrder (closeOrder inst)
appendToLog Trace $ [t|exitAtLimit: %?, deadline: %?|] (posTicker position) (timeToCancel `addUTCTime` lastTs) appendToLog $ [t|exitAtLimit: %?, deadline: %?|] (posTicker position) (timeToCancel `addUTCTime` lastTs)
modifyPosition (\pos -> modifyPosition (\pos ->
pos { posCurrentOrder = Nothing, pos { posCurrentOrder = Nothing,
posState = PositionWaitingCloseSubmission (closeOrder inst roundedPrice) { orderId = oid }, posState = PositionWaitingCloseSubmission (closeOrder inst),
posNextState = Just PositionClosed, posNextState = Just PositionClosed,
posSubmissionDeadline = Just $ 10 `addUTCTime` lastTs, posSubmissionDeadline = Just $ 10 `addUTCTime` lastTs,
posExecutionDeadline = Just $ timeToCancel `addUTCTime` lastTs }) position posExecutionDeadline = Just $ timeToCancel `addUTCTime` lastTs }) position
Nothing -> do
appendToLog Warning $ "Unable to locate first bar series"
return position
where where
closeOrder inst roundedPrice = mkOrder { closeOrder inst = mkOrder {
orderAccountId = posAccount position, orderAccountId = posAccount position,
orderSecurity = posTicker position, orderSecurity = posTicker position,
orderQuantity = (abs . posBalance) position, orderQuantity = (abs . posBalance) position,
orderPrice = Limit roundedPrice, orderPrice = Limit price,
orderOperation = if posBalance position > 0 then Sell else Buy, orderOperation = if posBalance position > 0 then Sell else Buy,
orderSignalId = SignalId inst operationSignalName "" orderSignalId = SignalId inst operationSignalName ""
} }

40
src/ATrade/RoboCom/Types.hs

@ -1,4 +1,3 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@ -7,48 +6,41 @@
module ATrade.RoboCom.Types ( module ATrade.RoboCom.Types (
Bar(..), Bar(..),
BarSeriesId(..),
BarSeries(..), BarSeries(..),
Timeframe(..),
tfSeconds,
Ticker(..), Ticker(..),
Bars, Bars,
TickerInfoMap, InstrumentParameters(..)
InstrumentParameters(..),
bsidTickerId,
barSeriesId
) where ) where
import ATrade.Types import ATrade.Types
import Control.Lens.Setter (over)
import Control.Lens.Tuple (_1)
import Data.Aeson import Data.Aeson
import Data.Aeson.Key (fromText, toText)
import Data.Aeson.KeyMap as KM
import Data.Aeson.Types import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import qualified Data.Text as T import qualified Data.Text as T
import GHC.Generics (Generic)
newtype Timeframe =
Timeframe Integer deriving (Show, Eq)
tfSeconds :: (Num a) => Timeframe -> a
tfSeconds (Timeframe s) = fromInteger s
data InstrumentParameters = data InstrumentParameters =
InstrumentParameters { InstrumentParameters {
ipTickerId :: TickerId,
ipLotSize :: Int, ipLotSize :: Int,
ipTickSize :: Price ipTickSize :: Price
} deriving (Show, Eq) } deriving (Show, Eq)
type TickerInfoMap = M.Map TickerId InstrumentParameters
data BarSeries = data BarSeries =
BarSeries { BarSeries {
bsTickerId :: TickerId, bsTickerId :: TickerId,
bsTimeframe :: BarTimeframe, bsTimeframe :: Timeframe,
bsBars :: [Bar], bsBars :: [Bar],
bsParams :: InstrumentParameters bsParams :: InstrumentParameters
} deriving (Show, Eq) } deriving (Show, Eq)
barSeriesId :: BarSeries -> BarSeriesId
barSeriesId s = BarSeriesId (bsTickerId s) (bsTimeframe s)
-- | Ticker description record -- | Ticker description record
data Ticker = Ticker { data Ticker = Ticker {
code :: T.Text, -- ^ Main ticker code, which is used to make orders and tick parsing code :: T.Text, -- ^ Main ticker code, which is used to make orders and tick parsing
@ -67,20 +59,14 @@ instance FromJSON Ticker where
return $ Ticker nm als' tf) return $ Ticker nm als' tf)
where where
parseAliases :: Value -> Parser [(String, String)] parseAliases :: Value -> Parser [(String, String)]
parseAliases = withObject "object1" (mapM (parseAlias . over _1 toText) . KM.toList) parseAliases = withObject "object1" (mapM parseAlias . HM.toList)
parseAlias :: (T.Text, Value) -> Parser (String, String) parseAlias :: (T.Text, Value) -> Parser (String, String)
parseAlias (k, v) = withText "string1" (\s -> return (T.unpack k, T.unpack s)) v parseAlias (k, v) = withText "string1" (\s -> return (T.unpack k, T.unpack s)) v
instance ToJSON Ticker where instance ToJSON Ticker where
toJSON t = object [ "name" .= code t, toJSON t = object [ "name" .= code t,
"timeframe" .= timeframeSeconds t, "timeframe" .= timeframeSeconds t,
"aliases" .= Object (KM.fromList $ fmap (\(x, y) -> (fromText . T.pack $ x, String $ T.pack y)) $ aliases t) ] "aliases" .= Object (HM.fromList $ fmap (\(x, y) -> (T.pack x, String $ T.pack y)) $ aliases t) ]
data BarSeriesId = BarSeriesId TickerId BarTimeframe
deriving (Show, Eq, Generic, Ord)
bsidTickerId :: BarSeriesId -> TickerId
bsidTickerId (BarSeriesId tid _) = tid
type Bars = M.Map BarSeriesId BarSeries type Bars = M.Map TickerId BarSeries

7
src/ATrade/RoboCom/Utils.hs

@ -20,7 +20,6 @@ import qualified Data.Text as T
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import Data.Int (Int64)
import Text.Read hiding (String) import Text.Read hiding (String)
rescaleToDaily :: [Bar] -> [Bar] rescaleToDaily :: [Bar] -> [Bar]
@ -37,13 +36,13 @@ rescaleToDaily (firstBar:restBars) = rescaleToDaily' restBars firstBar
rescaleToDaily [] = [] rescaleToDaily [] = []
barEndTime :: Bar -> Int64 -> UTCTime barEndTime :: Bar -> Integer -> UTCTime
barEndTime bar tframe = addUTCTime (fromIntegral $ (1 + barNumber (barTimestamp bar) tframe) * tframe) epoch barEndTime bar tframe = addUTCTime (fromIntegral $ (1 + barNumber (barTimestamp bar) tframe) * tframe) epoch
barStartTime :: Bar -> Int64 -> UTCTime barStartTime :: Bar -> Integer -> UTCTime
barStartTime bar tframe = addUTCTime (fromIntegral $ barNumber (barTimestamp bar) tframe * tframe) epoch barStartTime bar tframe = addUTCTime (fromIntegral $ barNumber (barTimestamp bar) tframe * tframe) epoch
barNumber :: UTCTime -> Int64 -> Int64 barNumber :: UTCTime -> Integer -> Integer
barNumber ts barlen = floor (diffUTCTime ts epoch) `div` barlen barNumber ts barlen = floor (diffUTCTime ts epoch) `div` barlen
epoch :: UTCTime epoch :: UTCTime

5
stack.yaml

@ -18,7 +18,7 @@
# #
# resolver: ./custom-snapshot.yaml # resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-20.26 resolver: lts-17.14
# User packages to be built. # User packages to be built.
# Various formats can be used as shown in the example below. # Various formats can be used as shown in the example below.
@ -48,9 +48,6 @@ extra-deps:
- binary-ieee754-0.1.0.0 - binary-ieee754-0.1.0.0
- th-printf-0.7 - th-printf-0.7
- normaldistribution-1.1.0.3 - normaldistribution-1.1.0.3
- co-log-0.5.0.0
- chronos-1.1.5@sha256:ca35be5fdbbb384414226b4467c6d1c8b44defe59a9c8a3af32c1c5fb250c781,3830
- typerep-map-0.5.0.0@sha256:34f1ba9b268a6d52e26ae460011a5571e8099b50a3f4a7c8db25dd8efe3be8ee,4667
# Override default flag values for local packages and extra-deps # Override default flag values for local packages and extra-deps
# flags: {} # flags: {}

2
test/ArbitraryInstances.hs

@ -52,7 +52,7 @@ instance Arbitrary OrderPrice where
| v == 2 -> Limit <$> arbitrary `suchThat` notTooBig | v == 2 -> Limit <$> arbitrary `suchThat` notTooBig
| v == 3 -> Stop <$> arbitrary `suchThat` notTooBig <*> arbitrary `suchThat` notTooBig | v == 3 -> Stop <$> arbitrary `suchThat` notTooBig <*> arbitrary `suchThat` notTooBig
| v == 4 -> StopMarket <$> arbitrary `suchThat` notTooBig | v == 4 -> StopMarket <$> arbitrary `suchThat` notTooBig
| otherwise -> error "invalid case" | otherwise -> fail "Invalid case"
instance Arbitrary Operation where instance Arbitrary Operation where
arbitrary = elements [Buy, Sell] arbitrary = elements [Buy, Sell]

6
test/Spec.hs

@ -1,6 +1,6 @@
import qualified Test.BarAggregator import qualified Test.BarAggregator
import qualified Test.Driver.Junction.QuoteThread
import qualified Test.RoboCom.Indicators import qualified Test.RoboCom.Indicators
import qualified Test.RoboCom.Positions
import qualified Test.RoboCom.Utils import qualified Test.RoboCom.Utils
import Test.Tasty import Test.Tasty
@ -11,9 +11,9 @@ main = defaultMain $ testGroup "Tests" [unitTests, properties]
unitTests :: TestTree unitTests :: TestTree
unitTests = testGroup "Unit Tests" unitTests = testGroup "Unit Tests"
[Test.RoboCom.Indicators.unitTests, [Test.RoboCom.Indicators.unitTests,
Test.RoboCom.Positions.unitTests,
Test.RoboCom.Utils.unitTests, Test.RoboCom.Utils.unitTests,
Test.BarAggregator.unitTests, Test.BarAggregator.unitTests ]
Test.Driver.Junction.QuoteThread.unitTests]
properties :: TestTree properties :: TestTree
properties = testGroup "Properties" properties = testGroup "Properties"

228
test/Test/BarAggregator.hs

@ -10,21 +10,18 @@ import ATrade.BarAggregator
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Data.List import Data.List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import Safe import Safe
import Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Test.Tasty import Test.Tasty
import Test.Tasty.Hedgehog
import Test.Tasty.HUnit import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Test.Tasty.SmallCheck as SC
import ArbitraryInstances
unitTests = testGroup "BarAggregator" [ unitTests = testGroup "BarAggregator" [
@ -32,41 +29,17 @@ unitTests = testGroup "BarAggregator" [
, testOneTick , testOneTick
, testTwoTicksInSameBar , testTwoTicksInSameBar
, testTwoTicksInDifferentBars , testTwoTicksInDifferentBars
, testOneBar
, testTwoBarsInSameBar
, testTwoBarsInSameBarLastBar
, testNextBarAfterBarClose
, testUpdateTime
] ]
properties = testGroup "BarAggregator" [ properties = testGroup "BarAggregator" [
prop_allTicksInOneBar prop_allTicksInOneBar
, prop_threeBars
] ]
secParams = InstrumentParameters "TEST_TICKER" 1 0.01
genTick :: T.Text -> UTCTime -> Int -> Gen Tick
genTick tickerId baseTime timeframe = do
ts <- generateTimestampInsideBar baseTime timeframe
val <- fromIntegral <$> Gen.int (Range.linear 1 1000000)
vol <- Gen.integral (Range.linear 1 1000000)
return $ Tick tickerId LastTradePrice ts (fromDouble $ val / 1000) vol
where
generateTimestampInsideBar base timeframe =
flip addUTCTime base .
fromRational .
toRational .
picosecondsToDiffTime <$> Gen.integral (Range.linear 0 (truncate 1e12 * fromIntegral timeframe))
mkAggregator :: TickerId -> Int -> BarAggregator
mkAggregator tickerId tf = mkAggregatorFromBars (M.singleton tickerId (BarSeries tickerId (BarTimeframe tf) [] secParams)) [(0, 86400)]
assertBarCorrespondence :: (MonadTest m) => Bar -> NE.NonEmpty Tick -> m ()
assertBarCorrespondence bar ticks = do
barHigh bar === maximum (value <$> sortedTicks)
barLow bar === minimum (value <$> sortedTicks)
barOpen bar === value (NE.head sortedTicks)
barClose bar === value (NE.last sortedTicks)
barVolume bar === sum (volume <$> sortedTicks)
where
sortedTicks = NE.fromList . sortOn timestamp . NE.toList $ ticks
testUnknownBarSeries :: TestTree testUnknownBarSeries :: TestTree
testUnknownBarSeries = testCase "Tick with unknown ticker id" $ do testUnknownBarSeries = testCase "Tick with unknown ticker id" $ do
let agg = BarAggregator M.empty M.empty [(0, 86400)] let agg = BarAggregator M.empty M.empty [(0, 86400)]
@ -84,7 +57,7 @@ testUnknownBarSeries = testCase "Tick with unknown ticker id" $ do
testOneTick :: TestTree testOneTick :: TestTree
testOneTick = testCase "One tick" $ do testOneTick = testCase "One tick" $ do
let series = BarSeries "TEST_TICKER" (BarTimeframe 60) [] secParams let series = BarSeries "TEST_TICKER" (Timeframe 60) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)] let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (mbar, newagg) = handleTick tick agg let (mbar, newagg) = handleTick tick agg
mbar @?= Nothing mbar @?= Nothing
@ -100,7 +73,7 @@ testOneTick = testCase "One tick" $ do
testTwoTicksInSameBar :: TestTree testTwoTicksInSameBar :: TestTree
testTwoTicksInSameBar = testCase "Two ticks - same bar" $ do testTwoTicksInSameBar = testCase "Two ticks - same bar" $ do
let series = BarSeries "TEST_TICKER" (BarTimeframe 60) [] secParams let series = BarSeries "TEST_TICKER" (Timeframe 60) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)] let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (mbar, newagg) = handleTick (tick testTimestamp1 12.00) agg let (mbar, newagg) = handleTick (tick testTimestamp1 12.00) agg
mbar @?= Nothing mbar @?= Nothing
@ -119,18 +92,16 @@ testTwoTicksInSameBar = testCase "Two ticks - same bar" $ do
testTwoTicksInDifferentBars :: TestTree testTwoTicksInDifferentBars :: TestTree
testTwoTicksInDifferentBars = testCase "Two ticks - different bar" $ do testTwoTicksInDifferentBars = testCase "Two ticks - different bar" $ do
let series = BarSeries "TEST_TICKER" (BarTimeframe 60) [] secParams let series = BarSeries "TEST_TICKER" (Timeframe 60) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)] let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (mbar, newagg) = handleTick (tick testTimestamp1 12.00) agg let (mbar, newagg) = handleTick (tick testTimestamp1 12.00) agg
mbar @?= Nothing mbar @?= Nothing
let (mbar', newagg') = handleTick (tick testTimestamp2 14.00) newagg let (mbar', newagg') = handleTick (tick testTimestamp2 14.00) newagg
mbar' @?= Just (Bar "TEST_TICKER" barEndTime 12.00 12.00 12.00 12.00 1) mbar' @?= Just (Bar "TEST_TICKER" testTimestamp1 12.00 12.00 12.00 12.00 1)
(bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg')) @?= Just [Bar "TEST_TICKER" testTimestamp2 14.00 14.00 14.00 14.00 1, Bar "TEST_TICKER" testTimestamp1 12.00 12.00 12.00 12.00 1]
(bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg')) @?= Just [Bar "TEST_TICKER" testTimestamp2 14.00 14.00 14.00 14.00 1, Bar "TEST_TICKER" barEndTime 12.00 12.00 12.00 12.00 1]
where where
testTimestamp1 = UTCTime (fromGregorian 1970 1 1) 58 testTimestamp1 = (UTCTime (fromGregorian 1970 1 1) 58)
barEndTime = UTCTime (fromGregorian 1970 1 1) 60 testTimestamp2 = (UTCTime (fromGregorian 1970 1 1) 61)
testTimestamp2 = UTCTime (fromGregorian 1970 1 1) 61
tick ts val = Tick { tick ts val = Tick {
security = "TEST_TICKER", security = "TEST_TICKER",
datatype = LastTradePrice, datatype = LastTradePrice,
@ -138,42 +109,141 @@ testTwoTicksInDifferentBars = testCase "Two ticks - different bar" $ do
value = fromDouble val, value = fromDouble val,
volume = 1 } volume = 1 }
prop_allTicksInOneBar :: TestTree testOneBar :: TestTree
prop_allTicksInOneBar = testProperty "All ticks in one bar" $ property $ do testOneBar = testCase "One bar" $ do
tf <- forAll $ Gen.integral (Range.constant 1 86400) let series = BarSeries "TEST_TICKER" (Timeframe 3600) []
ticks <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" baseTime tf) let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let ticks' = sortOn timestamp ticks let (mbar, newagg) = handleBar bar agg
let (newbars, agg) = handleTicks ticks' (mkAggregator "TEST_TICKER" tf) mbar @?= Nothing
let (Just lastBar) = currentBar "TEST_TICKER" agg (bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg)) @?= Just [Bar "TEST_TICKER" testTimestamp 12.00 18.00 10.00 12.00 68]
HH.assert $ null newbars
assertBarCorrespondence lastBar $ NE.fromList ticks
where where
currentBar tickerId agg = headMay =<< (bsBars <$> M.lookup tickerId (bars agg)) testTimestamp = (UTCTime (fromGregorian 1970 1 1) 60)
baseTime = UTCTime (fromGregorian 1970 1 1) 0 bar = Bar {
barSecurity = "TEST_TICKER",
prop_threeBars :: TestTree barTimestamp = testTimestamp,
prop_threeBars = testProperty "Three bars" $ property $ do barOpen = fromDouble 12.00,
tf <- forAll $ Gen.integral (Range.constant 1 86400) barHigh = fromDouble 18.00,
barLow = fromDouble 10.00,
ticks1 <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" baseTime tf) barClose = fromDouble 12.00,
barVolume = 68 }
let secondBarBaseTime = addUTCTime (fromIntegral tf) baseTime
ticks2 <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" secondBarBaseTime tf)
testTwoBarsInSameBar :: TestTree
let thirdBarBaseTime = addUTCTime (fromIntegral $ 2 * tf) baseTime testTwoBarsInSameBar = testCase "Two bars (smaller timeframe) - same bar" $ do
ticks3 <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" thirdBarBaseTime tf) let series = BarSeries "TEST_TICKER" (Timeframe 600) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let ticks' = sortOn timestamp $ ticks1 <> ticks2 <> ticks3 let (mbar, newagg) = handleBar (bar testTimestamp1 12.00 13.00 10.00 11.00 1) agg
let ([secondBar, firstBar], agg) = handleTicks ticks' (mkAggregator "TEST_TICKER" tf) mbar @?= Nothing
let (mbar', newagg') = handleBar (bar testTimestamp2 12.00 15.00 11.00 12.00 2) newagg
assertBarCorrespondence firstBar (NE.fromList ticks1) mbar' @?= Nothing
assertBarCorrespondence secondBar (NE.fromList ticks2) (bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg')) @?= Just [Bar "TEST_TICKER" testTimestamp2 12.00 15.00 10.00 12.00 3]
where
barTimestamp firstBar === secondBarBaseTime testTimestamp1 = (UTCTime (fromGregorian 1970 1 1) 60)
barTimestamp secondBar === thirdBarBaseTime testTimestamp2 = (UTCTime (fromGregorian 1970 1 1) 120)
bar ts o h l c v = Bar {
barSecurity = "TEST_TICKER",
barTimestamp = ts,
barOpen = fromDouble o,
barHigh = fromDouble h,
barLow = fromDouble l,
barClose = fromDouble c,
barVolume = v }
testTwoBarsInSameBarLastBar :: TestTree
testTwoBarsInSameBarLastBar = testCase "Two bars (smaller timeframe) - same bar: last bar is exactly at the end of the bigger tf bar" $ do
let series = BarSeries "TEST_TICKER" (Timeframe 600) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (mbar, newagg) = handleBar (bar testTimestamp1 12.00 13.00 10.00 11.00 1) agg
mbar @?= Nothing
let (mbar', newagg') = handleBar (bar testTimestamp2 12.00 15.00 11.00 12.00 2) newagg
let expectedBar = Bar "TEST_TICKER" testTimestamp2 12.00 15.00 10.00 12.00 3
mbar' @?= Just expectedBar
(head . tail <$> bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg')) @?= Just expectedBar
where
testTimestamp1 = (UTCTime (fromGregorian 1970 1 1) 560)
testTimestamp2 = (UTCTime (fromGregorian 1970 1 1) 600)
bar ts o h l c v = Bar {
barSecurity = "TEST_TICKER",
barTimestamp = ts,
barOpen = fromDouble o,
barHigh = fromDouble h,
barLow = fromDouble l,
barClose = fromDouble c,
barVolume = v }
testNextBarAfterBarClose :: TestTree
testNextBarAfterBarClose = testCase "Three bars (smaller timeframe) - next bar after bigger tf bar close" $ do
let series = BarSeries "TEST_TICKER" (Timeframe 600) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (_, newagg) = handleBar (bar testTimestamp1 12.00 13.00 10.00 11.00 1) agg
let (_, newagg') = handleBar (bar testTimestamp2 12.00 15.00 11.00 12.00 2) newagg
let (_, newagg'') = handleBar (bar testTimestamp3 12.00 15.00 11.00 12.00 12) newagg'
let expectedBar = Bar "TEST_TICKER" testTimestamp3 12.00 15.00 11.00 12.00 12
(head <$> bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg'')) @?= Just expectedBar
where
testTimestamp1 = (UTCTime (fromGregorian 1970 1 1) 560)
testTimestamp2 = (UTCTime (fromGregorian 1970 1 1) 600)
testTimestamp3 = (UTCTime (fromGregorian 1970 1 1) 660)
bar ts o h l c v = Bar {
barSecurity = "TEST_TICKER",
barTimestamp = ts,
barOpen = fromDouble o,
barHigh = fromDouble h,
barLow = fromDouble l,
barClose = fromDouble c,
barVolume = v }
testUpdateTime :: TestTree
testUpdateTime = testCase "updateTime - next bar - creates new bar with zero volume" $ do
let series = BarSeries "TEST_TICKER" (Timeframe 3600) []
let agg = mkAggregatorFromBars (M.fromList [("TEST_TICKER", series)]) [(0, 86400)]
let (_, newagg) = handleBar (bar testTimestamp1 12.00 13.00 10.00 11.00 1) agg
let (_, newagg') = handleBar (bar testTimestamp2 12.00 15.00 11.00 12.00 2) newagg
let (newBar, newagg'') = updateTime (tick testTimestamp4 13.00 100) newagg'
let expectedNewBar = Bar "TEST_TICKER" testTimestamp2 12.00 15.00 10.00 12.00 3
let expectedBar = Bar "TEST_TICKER" testTimestamp4 13.00 13.00 13.00 13.00 0
(head <$> bsBars <$> (M.lookup "TEST_TICKER" $ bars newagg'')) @?= Just expectedBar
newBar @?= Just expectedNewBar
where
testTimestamp1 = (UTCTime (fromGregorian 1970 1 1) 560)
testTimestamp2 = (UTCTime (fromGregorian 1970 1 1) 600)
testTimestamp3 = (UTCTime (fromGregorian 1970 1 1) 3600)
testTimestamp4 = (UTCTime (fromGregorian 1970 1 1) 3660)
tick ts v vol = Tick {
security = "TEST_TICKER"
, datatype = LastTradePrice
, timestamp = ts
, value = v
, volume = vol }
bar ts o h l c v = Bar {
barSecurity = "TEST_TICKER",
barTimestamp = ts,
barOpen = fromDouble o,
barHigh = fromDouble h,
barLow = fromDouble l,
barClose = fromDouble c,
barVolume = v }
let (Just lastBar) = currentBar "TEST_TICKER" agg prop_allTicksInOneBar :: TestTree
assertBarCorrespondence lastBar (NE.fromList ticks3) prop_allTicksInOneBar = QC.testProperty "All ticks in one bar" $ QC.forAll (QC.choose (1, 86400)) $ \timeframe ->
QC.forAll (QC.listOf1 (genTick "TEST_TICKER" baseTime timeframe)) $ \ticks ->
let ticks' = sortOn timestamp ticks in
let (newbars, agg) = handleTicks ticks' (mkAggregator "TEST_TICKER" timeframe) in
null newbars &&
((barHigh <$> currentBar "TEST_TICKER" agg) == Just (maximum $ value <$> ticks)) &&
((barLow <$> currentBar "TEST_TICKER" agg) == Just (minimum $ value <$> ticks)) &&
((barOpen <$> currentBar "TEST_TICKER" agg) == (value <$> headMay ticks')) &&
((barClose <$> currentBar "TEST_TICKER" agg) == (value <$> lastMay ticks')) &&
((barVolume <$> currentBar "TEST_TICKER" agg) == Just (sum $ volume <$> ticks))
where where
genTick :: T.Text -> UTCTime -> Integer -> Gen Tick
genTick tickerId base tf = do
difftime <- fromRational . toRational . picosecondsToDiffTime <$> choose (0, truncate 1e12 * tf)
val <- arbitrary
vol <- arbitrary `suchThat` (> 0)
return $ Tick tickerId LastTradePrice (difftime `addUTCTime` baseTime) val vol
mkAggregator tickerId tf = mkAggregatorFromBars (M.singleton tickerId (BarSeries tickerId (Timeframe tf) [])) [(0, 86400)]
currentBar tickerId agg = headMay =<< (bsBars <$> M.lookup tickerId (bars agg)) currentBar tickerId agg = headMay =<< (bsBars <$> M.lookup tickerId (bars agg))
baseTime = UTCTime (fromGregorian 1970 1 1) 0 baseTime = UTCTime (fromGregorian 1970 1 1) 0

117
test/Test/Driver/Junction/QuoteThread.hs

@ -1,117 +0,0 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Test.Driver.Junction.QuoteThread
(
unitTests
) where
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Test.Tasty.SmallCheck as SC
import ATrade.Driver.Junction.QuoteThread (addSubscription,
startQuoteThread,
stopQuoteThread)
import ATrade.Logging (Message)
import ATrade.Quotes.HistoryProvider (HistoryProvider (..))
import ATrade.Quotes.TickerInfoProvider (TickerInfoProvider (..))
import ATrade.QuoteSource.Client (QuoteData (QDBar))
import ATrade.QuoteSource.Server (QuoteSourceServerData (..),
startQuoteSourceServer,
stopQuoteSourceServer)
import ATrade.RoboCom.Types (BarSeries (bsBars),
BarSeriesId (BarSeriesId),
InstrumentParameters (InstrumentParameters))
import ATrade.Types
import Colog.Core (LogAction (..))
import Colog.Core.Class (HasLog (..))
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.BoundedChan (newBoundedChan, readChan,
writeChan)
import Control.Exception (bracket)
import Control.Monad (forever)
import Control.Monad.Reader
import Data.IORef (IORef, newIORef, readIORef)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Time (UTCTime (UTCTime),
fromGregorian)
import System.IO (BufferMode (LineBuffering),
hSetBuffering, stderr)
import System.ZMQ4 (withContext)
import Test.Mock.HistoryProvider (MockHistoryProvider,
mkMockHistoryProvider,
mockGetHistory)
import Test.Mock.TickerInfoProvider (MockTickerInfoProvider,
mkMockTickerInfoProvider,
mockGetInstrumentParameters)
data TestEnv =
TestEnv
{
historyProvider :: MockHistoryProvider,
tickerInfoProvider :: MockTickerInfoProvider
}
type TestM = ReaderT TestEnv IO
instance HistoryProvider TestM where
getHistory tid tf from to = do
hp <- asks historyProvider
liftIO $ mockGetHistory hp tid tf from to
instance TickerInfoProvider TestM where
getInstrumentParameters tickers = do
tip <- asks tickerInfoProvider
liftIO $ mockGetInstrumentParameters tip tickers
instance HasLog TestEnv Message TestM where
getLogAction env = LogAction $ \msg -> return ()
qsEndpoint = "inproc://qs"
mockHistoryProvider = mkMockHistoryProvider $ M.fromList [(BarSeriesId "FOO" (BarTimeframe 3600), bars)]
where
bars = []
mockTickerInfoProvider = mkMockTickerInfoProvider $ M.fromList [("FOO", InstrumentParameters "FOO" 10 0.1)]
unitTests = testGroup "Driver.Junction.QuoteThread" [
testSubscription
]
testSubscription :: TestTree
testSubscription = testCase "Subscription" $ withContext $ \ctx -> do
barsRef <- newIORef M.empty
tiRef <- newIORef M.empty
serverChan <- newBoundedChan 2000
let clientSecurityParams = defaultClientSecurityParams
bracket
(startQuoteSourceServer serverChan ctx qsEndpoint defaultServerSecurityParams)
stopQuoteSourceServer $ \_ ->
bracket
(startQuoteThread barsRef tiRef ctx qsEndpoint clientSecurityParams (`runReaderT` (TestEnv mockHistoryProvider mockTickerInfoProvider)) (LogAction $ \_ -> return ()))
stopQuoteThread $ \qt -> do
chan <- newBoundedChan 2000
addSubscription qt "FOO" (BarTimeframe 3600) chan
forkIO $ forever $ threadDelay 50000 >> writeChan serverChan (QSSBar (BarTimeframe 3600, bar))
clientData <- readChan chan
assertEqual "Invalid client data" clientData (QDBar (BarTimeframe 3600, bar))
bars <- readIORef barsRef
case M.lookup (BarSeriesId "FOO" (BarTimeframe 3600)) bars of
Just series -> assertBool "Length should be >= 1" $ (not . null . bsBars) series
Nothing -> assertFailure "Bar Series should be present"
where
bar =
Bar {
barSecurity="FOO", barTimestamp=UTCTime (fromGregorian 2021 11 20) 7200, barOpen=10, barHigh=12, barLow=9, barClose=11, barVolume=100
}

27
test/Test/Mock/HistoryProvider.hs

@ -1,27 +0,0 @@
module Test.Mock.HistoryProvider
(
MockHistoryProvider,
mkMockHistoryProvider,
mockGetHistory
) where
import ATrade.Quotes.HistoryProvider
import ATrade.RoboCom.Types (BarSeriesId (BarSeriesId), Bars)
import ATrade.Types (Bar (Bar, barTimestamp),
BarTimeframe (BarTimeframe),
TickerId)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Map.Strict as M
import Data.Time (UTCTime)
data MockHistoryProvider = MockHistoryProvider (M.Map BarSeriesId [Bar])
mkMockHistoryProvider :: M.Map BarSeriesId [Bar] -> MockHistoryProvider
mkMockHistoryProvider = MockHistoryProvider
mockGetHistory :: (MonadIO m) => MockHistoryProvider -> TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar]
mockGetHistory (MockHistoryProvider bars) tid tf from to =
case M.lookup (BarSeriesId tid tf) bars of
Just series -> return $ filter (\bar -> (barTimestamp bar >= from) && (barTimestamp bar <= to)) series
Nothing -> return []

22
test/Test/Mock/TickerInfoProvider.hs

@ -1,22 +0,0 @@
module Test.Mock.TickerInfoProvider
(
MockTickerInfoProvider,
mkMockTickerInfoProvider,
mockGetInstrumentParameters
) where
import ATrade.Quotes.TickerInfoProvider
import ATrade.RoboCom.Types (InstrumentParameters)
import ATrade.Types (TickerId)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, mapMaybe)
data MockTickerInfoProvider = MockTickerInfoProvider (M.Map TickerId InstrumentParameters)
mkMockTickerInfoProvider :: (M.Map TickerId InstrumentParameters) -> MockTickerInfoProvider
mkMockTickerInfoProvider = MockTickerInfoProvider
mockGetInstrumentParameters :: MockTickerInfoProvider -> [TickerId] -> IO [InstrumentParameters]
mockGetInstrumentParameters (MockTickerInfoProvider params) = return . mapMaybe (`M.lookup` params)

2
test/Test/RoboCom/Indicators.hs

@ -7,6 +7,8 @@ module Test.RoboCom.Indicators
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Test.Tasty.SmallCheck as SC
import ATrade.Types import ATrade.Types
import qualified Data.Text as T import qualified Data.Text as T

6
test/Test/RoboCom/Positions.hs

@ -8,13 +8,15 @@ module Test.RoboCom.Positions
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Test.Tasty.SmallCheck as SC
import ATrade.Types import ATrade.Types
import qualified Data.List as L
import qualified Data.Map.Strict as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Map.Strict as M
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import qualified Data.List as L
import ATrade.RoboCom.Monad import ATrade.RoboCom.Monad
import ATrade.RoboCom.Positions import ATrade.RoboCom.Positions

2
test/Test/RoboCom/Utils.hs

@ -7,6 +7,8 @@ module Test.RoboCom.Utils
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Test.Tasty.SmallCheck as SC
import ATrade.Types import ATrade.Types
import qualified Data.Text as T import qualified Data.Text as T

Loading…
Cancel
Save