Browse Source

Merge branch 'stable'

master
Denis Tereshkin 4 years ago
parent
commit
bd5ddfc8b6
  1. 18
      robocom-zero.cabal
  2. 102
      src/ATrade/Backtest/Execution.hs
  3. 108
      src/ATrade/BarAggregator.hs
  4. 248
      src/ATrade/Driver/Backtest.hs
  5. 58
      src/ATrade/Driver/Junction.hs
  6. 54
      src/ATrade/Driver/Junction/Types.hs
  7. 493
      src/ATrade/Driver/Real.hs
  8. 95
      src/ATrade/Driver/Real/BrokerClientThread.hs
  9. 38
      src/ATrade/Driver/Real/QuoteSourceThread.hs
  10. 39
      src/ATrade/Driver/Real/Types.hs
  11. 22
      src/ATrade/Driver/Types.hs
  12. 7
      src/ATrade/Exceptions.hs
  13. 153
      src/ATrade/Forums/Smartlab.hs
  14. 23
      src/ATrade/Quotes.hs
  15. 4
      src/ATrade/Quotes/Finam.hs
  16. 115
      src/ATrade/Quotes/HAP.hs
  17. 56
      src/ATrade/Quotes/QHP.hs
  18. 39
      src/ATrade/Quotes/QTIS.hs
  19. 27
      src/ATrade/RoboCom.hs
  20. 6
      src/ATrade/RoboCom/Indicators.hs
  21. 78
      src/ATrade/RoboCom/Monad.hs
  22. 139
      src/ATrade/RoboCom/Positions.hs
  23. 15
      src/ATrade/RoboCom/Types.hs
  24. 9
      src/ATrade/RoboCom/Utils.hs
  25. 6
      stack.yaml
  26. 66
      test/Test/BarAggregator.hs

18
robocom-zero.cabal

@ -1,5 +1,5 @@
name: robocom-zero name: robocom-zero
version: 0.1.0.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
@ -7,7 +7,7 @@ license: BSD3
license-file: LICENSE license-file: LICENSE
author: Denis Tereshkin author: Denis Tereshkin
maintainer: denis@kasan.ws maintainer: denis@kasan.ws
copyright: 2018 Denis Tereshkin copyright: 2021 Denis Tereshkin
category: Web category: Web
build-type: Simple build-type: Simple
extra-source-files: README.md extra-source-files: README.md
@ -21,15 +21,19 @@ library
, ATrade.RoboCom.Positions , ATrade.RoboCom.Positions
, ATrade.RoboCom.Types , ATrade.RoboCom.Types
, ATrade.RoboCom.Utils , ATrade.RoboCom.Utils
, ATrade.Quotes
, ATrade.Quotes.Finam , ATrade.Quotes.Finam
, ATrade.Quotes.HAP
, ATrade.Quotes.QHP , ATrade.Quotes.QHP
, ATrade.Quotes.QTIS , ATrade.Quotes.QTIS
, ATrade.Driver.Real , ATrade.Driver.Real
, ATrade.Driver.Backtest , ATrade.Driver.Backtest
, ATrade.Driver.Junction
, ATrade.Driver.Junction.Types
, ATrade.BarAggregator , ATrade.BarAggregator
, ATrade.RoboCom
other-modules: Paths_robocom_zero
build-depends: base >= 4.7 && < 5 build-depends: base >= 4.7 && < 5
, libatrade == 0.8.0.0 , libatrade >= 0.9.0.0 && < 0.10.0.0
, text , text
, text-icu , text-icu
, errors , errors
@ -49,7 +53,6 @@ library
, binary-ieee754 , binary-ieee754
, zeromq4-haskell , zeromq4-haskell
, unordered-containers , unordered-containers
, ether
, th-printf , th-printf
, BoundedChan , BoundedChan
, monad-loops , monad-loops
@ -63,12 +66,15 @@ library
, signal , signal
, random , random
, hedis , hedis
, gitrev
, data-default
, template-haskell
default-language: Haskell2010 default-language: Haskell2010
other-modules: ATrade.Exceptions other-modules: ATrade.Exceptions
, ATrade.Driver.Real.BrokerClientThread , ATrade.Driver.Real.BrokerClientThread
, ATrade.Driver.Real.QuoteSourceThread , ATrade.Driver.Real.QuoteSourceThread
, ATrade.Driver.Real.Types , ATrade.Driver.Types
test-suite robots-test test-suite robots-test
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0

102
src/ATrade/Backtest/Execution.hs

@ -1,102 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module ATrade.Backtest.Execution (
mkExecutionAgent,
ExecutionAgent(..),
executePending,
executeStep
) where
import qualified Data.Text as T
import qualified Data.Map as M
import qualified Data.List as L
import ATrade.Types
import ATrade.Strategy.Types
import ATrade.Strategy
import Control.Monad.State
import Control.Monad.Trans.Writer
import Data.Decimal
import Data.Time.Clock
import Data.Time.Calendar
data Position = Position {
ticker :: T.Text,
balance :: Int }
data ExecutionAgent = ExecutionAgent {
pendingOrders :: [Order],
cash :: Decimal,
currentTime :: UTCTime,
orderIdCounter :: Integer
}
mkExecutionAgent startCash = ExecutionAgent { pendingOrders = [],
cash = startCash,
currentTime = UTCTime (fromGregorian 1970 1 1) 0,
orderIdCounter = 1 }
executeAtPrice :: Order -> Decimal -> WriterT [Event] (State ExecutionAgent) ()
executeAtPrice order price = do
when (orderState order == Unsubmitted) $ tell [OrderSubmitted order]
tell [OrderUpdate (orderId order) Executed]
timestamp <- gets currentTime
tell [NewTrade (mkTradeForOrder timestamp order price)]
case orderOperation order of
Buy -> modify' (\agent -> agent { cash = cash agent - price * realFracToDecimal 10 (toRational $ orderQuantity order) })
Sell -> modify' (\agent -> agent { cash = cash agent + price * realFracToDecimal 10 (toRational $ orderQuantity order) })
mkTradeForOrder timestamp order price = Trade { tradeOrderId = orderId order,
tradePrice = price,
tradeQuantity = orderQuantity order,
tradeVolume = price * realFracToDecimal 10 (toRational $ orderQuantity order),
tradeVolumeCurrency = "TEST_CURRENCY",
tradeOperation = orderOperation order,
tradeAccount = orderAccountId order,
tradeSecurity = orderSecurity order,
tradeTimestamp = timestamp,
tradeSignalId = orderSignalId order }
executePending :: Bars -> WriterT [Event] (State ExecutionAgent) ()
executePending bars = do
orders <- gets pendingOrders
let (executedOrders, leftover) = L.partition shouldExecute orders
mapM_ executeAtOrdersPrice executedOrders
modify' (\s -> s { pendingOrders = leftover } )
where
executeAtOrdersPrice order = case orderPrice order of
Limit price -> executeAtPrice order price
_ -> return () -- TODO handle stops
shouldExecute order = case M.lookup (orderSecurity order) bars of
Just (DataSeries ((ts, bar) : _)) -> case orderPrice order of
Limit price -> crosses bar price
_ -> False
Nothing -> False
crosses bar price = (barClose bar > price && barOpen bar < price) || (barClose bar < price && barOpen bar > price)
executeStep :: Bars -> [Order] -> WriterT [Event] (State ExecutionAgent) ()
executeStep bars orders = do
-- Assign consecutive IDs
orders' <- mapM (\o -> do
id <- gets orderIdCounter
modify(\s -> s { orderIdCounter = id + 1 })
return o { orderId = id }) orders
let (executableNow, pending) = L.partition isExecutableNow orders'
mapM_ (executeOrderAtLastPrice bars) executableNow
modify' (\s -> s { pendingOrders = pending ++ pendingOrders s })
where
isExecutableNow order = case M.lookup (orderSecurity order) bars of
Just (DataSeries (x:xs)) -> case orderPrice order of
Limit price -> (orderOperation order == Buy && price >= (barClose . snd) x) || (orderOperation order == Sell && price <= (barClose . snd) x)
Market -> True
_ -> False
executeOrderAtLastPrice bars order = case M.lookup (orderSecurity order) bars of
Just (DataSeries ((ts, bar) : _)) -> executeAtPrice order (barClose bar)
_ -> return ()

108
src/ATrade/BarAggregator.hs

@ -1,4 +1,5 @@
{-# LANGUAGE BangPatterns #-} {-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}
{-| {-|
- Module : ATrade.BarAggregator - Module : ATrade.BarAggregator
@ -19,8 +20,10 @@ module ATrade.BarAggregator (
mkAggregatorFromBars, mkAggregatorFromBars,
handleTicks, handleTicks,
handleTick, handleTick,
updateTime,
handleBar, handleBar,
hmsToDiffTime hmsToDiffTime,
replaceHistory
) where ) where
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
@ -30,7 +33,6 @@ import Control.Lens
import Control.Monad.State import Control.Monad.State
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Time.Clock import Data.Time.Clock
import Debug.Trace
-- | Bar aggregator state -- | Bar aggregator state
data BarAggregator = BarAggregator { data BarAggregator = BarAggregator {
@ -46,6 +48,9 @@ mkAggregatorFromBars myBars timeWindows = BarAggregator {
lastTicks = M.empty, lastTicks = M.empty,
tickTimeWindows = timeWindows } tickTimeWindows = timeWindows }
replaceHistory :: BarAggregator -> M.Map TickerId BarSeries -> BarAggregator
replaceHistory agg bars' = agg { bars = bars' }
lBars :: (M.Map TickerId BarSeries -> Identity (M.Map TickerId BarSeries)) -> BarAggregator -> Identity BarAggregator lBars :: (M.Map TickerId BarSeries -> Identity (M.Map TickerId BarSeries)) -> BarAggregator -> Identity BarAggregator
lBars = lens bars (\s b -> s { bars = b }) lBars = lens bars (\s b -> s { bars = b })
@ -108,7 +113,7 @@ handleTick tick = runState $ do
else else
return Nothing return Nothing
where where
isInTimeInterval tick (a, b) = (utctDayTime . timestamp) tick >= a && (utctDayTime . timestamp) tick <= b isInTimeInterval tick' (a, b) = (utctDayTime . timestamp) tick' >= a && (utctDayTime . timestamp) tick' <= b
barFromTick !newtick = Bar { barSecurity = security newtick, barFromTick !newtick = Bar { barSecurity = security newtick,
barTimestamp = timestamp newtick, barTimestamp = timestamp newtick,
barOpen = value newtick, barOpen = value newtick,
@ -132,64 +137,65 @@ handleTick tick = runState $ do
where where
newTimestamp = timestamp newtick newTimestamp = timestamp newtick
emptyBarFrom !bar newtick = newBar updateTime :: Tick -> BarAggregator -> (Maybe Bar, BarAggregator)
where updateTime tick = runState $ do
newTimestamp = timestamp newtick lLastTicks %= M.insert (security tick, datatype tick) tick
newBar = Bar {
barSecurity = barSecurity bar,
barTimestamp = newTimestamp,
barOpen = barClose bar,
barHigh = barClose bar,
barLow = barClose bar,
barClose = barClose bar,
barVolume = 0 }
handleBar :: Bar -> BarAggregator -> (Maybe Bar, BarAggregator)
handleBar bar = runState $ do
tws <- gets tickTimeWindows tws <- gets tickTimeWindows
mybars <- gets bars mybars <- gets bars
if (any (isInTimeInterval bar) tws) if (any (isInTimeInterval tick) tws)
then then
case M.lookup (barSecurity bar) 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 currentBn = barNumber (barTimestamp b) (tfSeconds $ bsTimeframe series) let currentBn = barNumber (barTimestamp b) (tfSeconds $ bsTimeframe series)
if currentBn == barNumber (barTimestamp bar) (tfSeconds $ bsTimeframe series) let thisBn = barNumber (timestamp tick) (tfSeconds $ bsTimeframe series)
then do if
lBars %= M.insert (barSecurity bar) series { bsBars = updateBar b bar : bs } | currentBn == thisBn -> do
return Nothing lBars %= M.insert (security tick) series { bsBars = updateBarTimestamp b tick : bs }
else return Nothing
if barEndTime b (tfSeconds $ bsTimeframe series) == barTimestamp bar | currentBn < thisBn -> do
then do lBars %= M.insert (security tick) series { bsBars = emptyBarFromTick tick : b : bs }
lBars %= M.insert (barSecurity bar) series { bsBars = emptyBarFrom bar : (updateBar b bar : bs) } return $ Just b
return . Just $ updateBar b bar | otherwise -> return Nothing
else do _ -> return Nothing
lBars %= M.insert (barSecurity bar) series { bsBars = bar : b : bs }
return . Just $ b
_ -> do
lBars %= M.insert (barSecurity bar) series { bsBars = [bar] }
return Nothing
_ -> return Nothing _ -> return Nothing
else else
return Nothing return Nothing
where where
isInTimeInterval bar' (a, b) = (utctDayTime . barTimestamp) bar' >= a && (utctDayTime . barTimestamp) bar' <= b isInTimeInterval t (a, b) = (utctDayTime . timestamp) t >= a && (utctDayTime . timestamp) t <= b
updateBar !bar' newbar = emptyBarFromTick !newtick = Bar { barSecurity = security newtick,
let newHigh = max (barHigh bar') (barHigh newbar) barTimestamp = timestamp newtick,
newLow = min (barLow bar') (barLow newbar) in barOpen = value newtick,
bar' { barHigh = value newtick,
barTimestamp = barTimestamp newbar, barLow = value newtick,
barHigh = newHigh, barClose = value newtick,
barLow = newLow, barVolume = 0 }
barClose = barClose newbar,
barVolume = barVolume bar' + (abs . barVolume $ newbar) } updateBarTimestamp !bar newtick = bar { barTimestamp = newTimestamp }
where
newTimestamp = timestamp newtick
handleBar :: Bar -> BarAggregator -> (Maybe Bar, BarAggregator)
handleBar bar = runState $ do
mybars <- gets bars
case M.lookup (barSecurity bar) mybars of
Just series -> case bsBars series of
(_:bs) -> do
lBars %= M.insert (barSecurity bar) series { bsBars = emptyBarFrom bar : bar : bs }
return . Just $ bar
_ -> do
lBars %= M.insert (barSecurity bar) series { bsBars = emptyBarFrom bar : [bar] }
return Nothing
_ -> return Nothing
where
emptyBarFrom bar' = Bar { emptyBarFrom bar' = Bar {
barSecurity = barSecurity bar', barSecurity = barSecurity bar',
barTimestamp = barTimestamp bar', barTimestamp = barTimestamp bar',
barOpen = barClose bar', barOpen = barClose bar',
barHigh = barClose bar', barHigh = barClose bar',
barLow = barClose bar', barLow = barClose bar',
barClose = barClose bar', barClose = barClose bar',
barVolume = 0 } barVolume = 0 }

248
src/ATrade/Driver/Backtest.hs

@ -1,46 +1,52 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-}
module ATrade.Driver.Backtest ( module ATrade.Driver.Backtest (
backtestMain backtestMain
) where ) where
import ATrade.Driver.Real.Types (InitializationCallback, import ATrade.Driver.Types (InitializationCallback,
Strategy (..),
StrategyInstanceParams (..)) StrategyInstanceParams (..))
import ATrade.Exceptions import ATrade.Exceptions
import ATrade.Quotes
import ATrade.Quotes.Finam as QF import ATrade.Quotes.Finam as QF
import ATrade.Quotes.QTIS
import ATrade.RoboCom.Monad (Event (..), EventCallback, import ATrade.RoboCom.Monad (Event (..), EventCallback,
StrategyAction (..), MonadRobot (..),
StrategyEnvironment (..), StrategyEnvironment (..),
runStrategyElement, st, appendToLog, seBars, seLastTimestamp)
appendToLog)
import ATrade.RoboCom.Positions import ATrade.RoboCom.Positions
import ATrade.RoboCom.Types (BarSeries (..), Ticker (..), import ATrade.RoboCom.Types (BarSeries (..), Bars, InstrumentParameters (InstrumentParameters),
Timeframe (..)) Ticker (..), Timeframe (..))
import ATrade.Types import ATrade.Types
import Conduit (awaitForever, runConduit, yield, import Conduit (awaitForever, runConduit, yield,
(.|)) (.|))
import Control.Exception.Safe import Control.Exception.Safe
import Control.Lens hiding (ix, (<|), (|>))
import Control.Monad.ST (runST) import Control.Monad.ST (runST)
import Control.Monad.State import Control.Monad.State
import Data.Aeson (FromJSON (..), Result (..), import Data.Aeson (FromJSON (..), Value (..), decode)
Value (..), decode)
import Data.Aeson.Types (parseMaybe) import Data.Aeson.Types (parseMaybe)
import Data.ByteString.Lazy (readFile, toStrict) import Data.ByteString.Lazy (readFile, toStrict)
import Data.Default
import Data.HashMap.Strict (lookup) import Data.HashMap.Strict (lookup)
import Data.List (concat, filter, find, partition) import Data.List (partition)
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.Semigroup ((<>)) import Data.Sequence (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.IO (putStrLn) import Data.Text.IO (putStrLn)
import qualified Data.Text.Lazy as TL
import Data.Time.Calendar (fromGregorian) import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (DiffTime, UTCTime (..)) import Data.Time.Clock (DiffTime, UTCTime (..))
import Data.Vector ((!), (!?), (//)) import Data.Vector ((!), (!?), (//))
@ -48,23 +54,39 @@ import qualified Data.Vector as V
import Options.Applicative hiding (Success) import Options.Applicative hiding (Success)
import Prelude hiding (lookup, putStrLn, readFile) import Prelude hiding (lookup, putStrLn, readFile)
import Safe (headMay) import Safe (headMay)
import System.ZMQ4 hiding (Event)
data Feed = Feed TickerId FilePath data Feed = Feed TickerId FilePath
deriving (Show, Eq) deriving (Show, Eq)
data Params = Params { data Params = Params {
strategyConfigFile :: FilePath, strategyConfigFile :: FilePath,
qtisEndpoint :: Maybe String, qtisEndpoint :: String,
paramsFeeds :: [Feed] paramsFeeds :: [Feed]
} deriving (Show, Eq) } deriving (Show, Eq)
data BacktestState c s = BacktestState {
_cash :: Double,
_robotState :: s,
_robotParams :: c,
_strategyEnvironment :: StrategyEnvironment,
_pendingOrders :: [Order],
_pendingEvents :: Seq Event,
_tradesLog :: [Trade],
_orderIdCounter :: Integer,
_pendingTimers :: [UTCTime],
_logs :: [T.Text]
}
makeLenses ''BacktestState
paramsParser :: Parser Params paramsParser :: Parser Params
paramsParser = Params paramsParser = Params
<$> strOption ( <$> strOption (
long "config" <> short 'c' long "config" <> short 'c'
) )
<*> optional ( 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'
)) ))
@ -74,8 +96,8 @@ 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)
backtestMain :: (FromJSON c, StateHasPositions s) => DiffTime -> s -> Maybe (InitializationCallback c) -> EventCallback c s -> IO () backtestMain :: (FromJSON c, StateHasPositions s) => DiffTime -> s -> EventCallback c s -> IO ()
backtestMain dataDownloadDelta defaultState initCallback callback = do backtestMain _dataDownloadDelta defaultState callback = do
params <- execParser opts params <- execParser opts
(tickerList, config) <- loadStrategyConfig params (tickerList, config) <- loadStrategyConfig params
@ -84,29 +106,34 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
strategyAccount = "foo", strategyAccount = "foo",
strategyVolume = 1, strategyVolume = 1,
tickers = tickerList, tickers = tickerList,
strategyQuotesourceEp = "", strategyQTISEp = Nothing }
strategyBrokerEp = "",
strategyHistoryProviderType = "",
strategyHistoryProvider = "",
strategyQTISEp = T.pack <$> qtisEndpoint params}
updatedConfig <- case initCallback of
Just cb -> cb config instanceParams
Nothing -> return config
feeds <- loadFeeds (paramsFeeds params) feeds <- loadFeeds (paramsFeeds params)
runBacktestDriver feeds config tickerList bars <- makeBars (T.pack $ qtisEndpoint params) tickerList
runBacktestDriver feeds config bars
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 -> [Ticker] -> IO (M.Map TickerId BarSeries)
makeBars qtisEp tickersList =
withContext $ \ctx ->
M.fromList <$> mapM (mkBarEntry ctx qtisEp) tickersList
mkBarEntry ctx qtisEp tickerEntry = do
info <- qtisGetTickersInfo ctx qtisEp (code tickerEntry)
return (code tickerEntry, BarSeries (code tickerEntry) (Timeframe (timeframeSeconds tickerEntry)) [] (InstrumentParameters (fromInteger $ tiLotSize info) (tiTickSize info)))
runBacktestDriver feeds params tickerList = do runBacktestDriver feeds params tickerList = do
let s = runConduit $ barStreamFromFeeds feeds .| backtestLoop let s = runConduit $ barStreamFromFeeds feeds .| backtestLoop
let finalState = execState (unBacktestingMonad s) $ defaultBacktestState defaultState params tickerList let finalState = execState (unBacktestingMonad s) $ defaultBacktestState defaultState params tickerList
print $ cash finalState print $ finalState ^. cash
print $ tradesLog finalState print $ finalState ^. tradesLog
forM_ (reverse . logs $ finalState) putStrLn forM_ (reverse $ finalState ^. logs) putStrLn
loadStrategyConfig :: (FromJSON c) => Params -> IO ([Ticker], c) loadStrategyConfig :: (FromJSON c) => Params -> IO ([Ticker], c)
loadStrategyConfig params = do loadStrategyConfig params = do
@ -121,14 +148,11 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
Object o -> do Object o -> do
mbTickers <- "tickers" `lookup` o mbTickers <- "tickers" `lookup` o
mbParams <- "params" `lookup` o mbParams <- "params" `lookup` o
tickers <- parseMaybe parseJSON mbTickers tickers' <- parseMaybe parseJSON mbTickers
params <- parseMaybe parseJSON mbParams params <- parseMaybe parseJSON mbParams
return (tickers, params) return (tickers', params)
_ -> Nothing _ -> Nothing
resultToMaybe (Error _) = Nothing
resultToMaybe (Success a) = Just a
barStreamFromFeeds feeds = case nextBar feeds of barStreamFromFeeds feeds = case nextBar feeds of
Just (bar, feeds') -> yield bar >> barStreamFromFeeds feeds' Just (bar, feeds') -> yield bar >> barStreamFromFeeds feeds'
_ -> return () _ -> return ()
@ -146,7 +170,6 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
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
curIx <- readSTRef minIx
curTs <- readSTRef minTs curTs <- readSTRef minTs
case feed of case feed of
x:_ -> case curTs of x:_ -> case curTs of
@ -160,53 +183,35 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
readSTRef minIx readSTRef minIx
backtestLoop = awaitForever (\bar -> do backtestLoop = awaitForever (\bar -> do
env <- gets strategyEnvironment _curState <- use robotState
let oldTimestamp = seLastTimestamp env _env <- gets _strategyEnvironment
let newTimestamp = barTimestamp bar let newTimestamp = barTimestamp bar
let newenv = env { seBars = updateBars (seBars env) bar, seLastTimestamp = newTimestamp } strategyEnvironment . seBars %= (flip updateBars bar)
curState <- gets robotState strategyEnvironment . seLastTimestamp .= newTimestamp
modify' (\s -> s { strategyEnvironment = newenv }) enqueueEvent (NewBar bar)
handleEvents [NewBar bar]) lift handleEvents)
handleEvents events = do handleEvents = do
newActions <- mapM handleEvent events events <- use pendingEvents
newEvents <- executeActions (concat newActions) case events of
unless (null newEvents) $ handleEvents newEvents x :<| xs -> do
pendingEvents .= xs
executeActions actions = concat <$> mapM executeAction actions handleEvent x
handleEvents
executeAction (ActionOrder order) = do _ -> return ()
oid <- nextOrderId
let submittedOrder = order { orderState = Submitted, orderId = oid }
modify' (\s -> s { pendingOrders = submittedOrder : pendingOrders s })
return [OrderSubmitted submittedOrder]
executeAction (ActionCancelOrder oid) = do
mbOrder <- find (\o -> orderId o == oid && orderState o == Submitted) <$> gets pendingOrders
case mbOrder of
Just _ -> do
modify' (\s -> s { pendingOrders = filter (\o -> orderId o == oid) (pendingOrders s)})
return [OrderUpdate oid Cancelled]
_ -> return []
executeAction (ActionLog t) = modify' (\s -> s { logs = t : logs s }) >> return []
executeAction (ActionSetupTimer t) = modify' (\s -> s { pendingTimers = t : pendingTimers s }) >> return []
executeAction (ActionIO _ _) = return []
executePendingOrders bar = do executePendingOrders bar = do
ev1 <- executeMarketOrders bar executeMarketOrders bar
ev2 <- executeLimitOrders bar executeLimitOrders bar
return $ ev1 ++ ev2
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
_ -> False) <$> gets pendingOrders _ -> False) <$> use pendingOrders
let (executableOrders, otherOrders) = partition (isExecutable bar) limitOrders let (executableOrders, otherOrders') = partition (isExecutable bar) limitOrders
modify' (\s -> s { pendingOrders = otherOrders } ) pendingOrders .= otherOrders' ++ otherOrders''
forM executableOrders $ \order -> forM_ executableOrders $ \order -> order `executeAtPrice` priceForLimitOrder order bar
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
@ -225,16 +230,19 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
_ -> 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) <$> gets pendingOrders (marketOrders, otherOrders) <- partition (\o -> orderPrice o == Market) <$> use pendingOrders
modify' (\s -> s { 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 <- seLastTimestamp <$> gets strategyEnvironment ts <- use $ strategyEnvironment . seLastTimestamp
modify' (\s -> s { tradesLog = mkTrade order price ts : tradesLog s }) let thisTrade = mkTrade order price ts
return $ OrderUpdate (orderId order) Executed tradesLog %= (\log' -> thisTrade : log')
pendingEvents %= (\s -> (OrderUpdate (orderId order) Executed) <| s)
pendingEvents %= (\s -> (NewTrade thisTrade) <| s)
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,
@ -250,21 +258,16 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
} }
handleEvent event@(NewBar bar) = do handleEvent event@(NewBar bar) = do
events <- executePendingOrders bar executePendingOrders bar
handleEvents -- This should pass OrderUpdate events to the callback before NewBar events
firedTimers <- fireTimers (barTimestamp bar) firedTimers <- fireTimers (barTimestamp bar)
actions <- concat <$> mapM handleEvent (events ++ map TimerFired firedTimers) mapM_ (\x -> enqueueEvent (TimerFired x)) firedTimers
actions' <- handleEvent' event handleEvent' event
return $ actions ++ actions' return ()
handleEvent event = handleEvent' event handleEvent event = handleEvent' event
handleEvent' event = do handleEvent' event = callback event
env <- gets strategyEnvironment
params <- gets robotParams
curState <- gets robotState
let (newState, actions, _) = runStrategyElement params curState env $ callback event
modify' (\s -> s { robotState = newState } )
return actions
updateBars barMap newbar = M.alter (\case updateBars barMap newbar = M.alter (\case
Nothing -> Just BarSeries { bsTickerId = barSecurity newbar, Nothing -> Just BarSeries { bsTickerId = barSecurity newbar,
@ -273,11 +276,11 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
Just bs -> Just bs { bsBars = updateBarList newbar (bsBars bs) }) (barSecurity newbar) barMap Just bs -> Just bs { bsBars = updateBarList newbar (bsBars bs) }) (barSecurity newbar) barMap
updateBarList newbar (_:bs) = newbar:newbar:bs updateBarList newbar (_:bs) = newbar:newbar:bs
updateBarList newbar _ = newbar:[newbar] updateBarList newbar _ = newbar:[newbar]
fireTimers ts = do fireTimers ts = do
(firedTimers, otherTimers) <- partition (< ts) <$> gets pendingTimers (firedTimers, otherTimers) <- partition (< ts) <$> use pendingTimers
modify' (\s -> s { pendingTimers = otherTimers }) pendingTimers .= otherTimers
return firedTimers return firedTimers
loadFeeds :: [Feed] -> IO (V.Vector [Bar]) loadFeeds :: [Feed] -> IO (V.Vector [Bar])
@ -290,28 +293,43 @@ backtestMain dataDownloadDelta defaultState initCallback callback = do
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)
nextOrderId = do
oid <- gets orderIdCounter
modify' (\s -> s { orderIdCounter = oid + 1 })
return oid
data BacktestState s c = BacktestState {
cash :: Double,
robotState :: s,
robotParams :: c,
strategyEnvironment :: StrategyEnvironment,
pendingOrders :: [Order],
tradesLog :: [Trade],
orderIdCounter :: Integer,
pendingTimers :: [UTCTime],
logs :: [T.Text]
}
defaultBacktestState s c tickerList = BacktestState 0 s c (StrategyEnvironment "" "" 1 tickers (UTCTime (fromGregorian 1970 1 1) 0)) [] [] 1 [] [] enqueueEvent event = pendingEvents %= (\s -> s |> event)
instance (Default c, Default s) => Default (BacktestState c s)
where where
tickers = M.fromList $ map (\x -> (code x, BarSeries (code x) (Timeframe (timeframeSeconds x)) [])) tickerList def = defaultBacktestState def def def
defaultBacktestState :: s -> c -> Bars -> BacktestState c s
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))
nextOrderId :: BacktestingMonad s c OrderId
nextOrderId = do
orderIdCounter += 1
use orderIdCounter
instance MonadRobot (BacktestingMonad c s) c s where
submitOrder order = do
oid <- nextOrderId
let orderWithId = order { orderId = oid }
pendingOrders %= ((:) orderWithId)
pendingEvents %= (\s -> s |> (OrderSubmitted orderWithId))
cancelOrder oid = do
orders <- use pendingOrders
let (matchingOrders, otherOrders) = partition (\o -> orderId o == oid) orders
case matchingOrders of
[] -> return ()
xs -> do
mapM_ (\o -> pendingEvents %= (\s -> s |> (OrderUpdate (orderId o) Cancelled))) xs
pendingOrders .= otherOrders
appendToLog txt = logs %= ((:) (TL.toStrict txt))
setupTimer time = pendingTimers %= ((:) time)
enqueueIOAction _actionId _action = error "Backtesting io actions is not supported"
getConfig = use robotParams
getState = use robotState
setState s = robotState .= s
getEnvironment = use strategyEnvironment

58
src/ATrade/Driver/Junction.hs

@ -0,0 +1,58 @@
module ATrade.Driver.Junction
(
junctionMain
) where
import ATrade.Driver.Junction.Types (StrategyDescriptor (..),
StrategyInstance (..),
StrategyInstanceDescriptor (..))
import Data.Aeson (decode)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.IORef
import qualified Data.Map.Strict as M
import qualified Data.Text as T
load :: T.Text -> IO B.ByteString
load = undefined
junctionMain :: M.Map T.Text StrategyDescriptor -> IO ()
junctionMain descriptors = do
parseOptions
instanceDescriptors <- undefined
strategies <- mkStrategies instanceDescriptors
start strategies
where
parseOptions = undefined
mkStrategies :: [StrategyInstanceDescriptor] -> IO [StrategyInstance]
mkStrategies = mapM mkStrategy
mkStrategy :: StrategyInstanceDescriptor -> IO StrategyInstance
mkStrategy desc = do
sState <- load (stateKey desc)
sCfg <- load (configKey desc)
case M.lookup (strategyId desc) descriptors of
Just (StrategyDescriptor _sName sCallback _sDefState) ->
case (decode $ BL.fromStrict sCfg, decode $ BL.fromStrict sState) of
(Just pCfg, Just pState) -> do
cfgRef <- newIORef pCfg
stateRef <- newIORef pState
return $ StrategyInstance
{
strategyInstanceId = strategyName desc,
strategyEventCallback = sCallback,
strategyState = stateRef,
strategyConfig = cfgRef
}
_ -> undefined
_ -> undefined
start = undefined

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

@ -0,0 +1,54 @@
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Junction.Types
(
StrategyDescriptor(..),
TickerConfig(..),
StrategyInstanceDescriptor(..),
StrategyInstance(..)
) where
import ATrade.RoboCom.Monad (EventCallback)
import ATrade.Types (BarTimeframe, TickerId)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.ByteString as B
import Data.IORef
import qualified Data.Text as T
data StrategyDescriptor =
forall c s. (FromJSON s, ToJSON s, FromJSON c) =>
StrategyDescriptor
{
baseStrategyName :: T.Text,
eventCallback :: EventCallback c s,
defaultState :: s
}
data TickerConfig =
TickerConfig
{
tickerId :: TickerId,
timeframe :: BarTimeframe
}
data StrategyInstanceDescriptor =
StrategyInstanceDescriptor
{
strategyId :: T.Text,
strategyName :: T.Text,
configKey :: T.Text,
stateKey :: T.Text,
logPath :: T.Text,
tickers :: [TickerConfig]
}
data StrategyInstance =
forall c s. (FromJSON s, ToJSON s, FromJSON c) =>
StrategyInstance
{
strategyInstanceId :: T.Text,
strategyEventCallback :: EventCallback c s,
strategyState :: IORef s,
strategyConfig :: IORef c
}

493
src/ATrade/Driver/Real.hs

@ -1,62 +1,69 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Real ( module ATrade.Driver.Real (
Strategy(..),
StrategyInstanceParams(..), StrategyInstanceParams(..),
robotMain, robotMain,
BigConfig(..), BigConfig(..),
mkBarStrategy,
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.Monad.IO.Class EventCallback,
import Control.Concurrent hiding (writeChan, readChan, writeList2Chan, yield) MonadRobot (..),
import Control.Concurrent.BoundedChan as BC StrategyEnvironment (..),
import Control.Exception seBars, seLastTimestamp)
import qualified Data.ByteString as BS import ATrade.RoboCom.Types (BarSeries (..), InstrumentParameters (..),
import qualified Data.ByteString.Lazy as BL Ticker (..),
import qualified Data.List as L Timeframe (..))
import qualified Data.Map as M import ATrade.RoboCom.Utils (fromHMS)
import qualified Data.Text as T import ATrade.Types
import Data.Text.Encoding import Control.Concurrent hiding (readChan,
import Data.Aeson writeChan,
import Data.IORef writeList2Chan, yield)
import Data.Time.Calendar import Control.Concurrent.BoundedChan as BC
import Data.Time.Clock import Control.Exception.Safe
import Data.Time.Clock.POSIX import Control.Lens hiding (Context, (.=))
import Data.Maybe import Control.Monad
import Data.Monoid import Control.Monad.Reader
import Database.Redis hiding (info, decode) import Data.Aeson
import ATrade.Types import qualified Data.ByteString as BS
import ATrade.RoboCom.Monad (StrategyMonad, StrategyAction(..), EventCallback, Event(..), runStrategyElement, StrategyEnvironment(..), Event(..), MonadRobot(..)) import qualified Data.ByteString.Lazy as BL
import ATrade.BarAggregator import Data.IORef
import ATrade.Driver.Real.BrokerClientThread import qualified Data.Map as M
import ATrade.Driver.Real.QuoteSourceThread import Data.Maybe
import ATrade.Driver.Real.Types (Strategy(..), StrategyInstanceParams(..), InitializationCallback) import qualified Data.Text as T
import ATrade.RoboCom.Types (BarSeries(..), Ticker(..), Timeframe(..)) import Data.Text.Encoding
import ATrade.Exceptions import qualified Data.Text.Lazy as TL
import ATrade.Quotes.Finam as QF import Data.Time.Calendar
import ATrade.Quotes.QHP as QQ import Data.Time.Clock
import ATrade.Quotes.HAP as QH import Data.Time.Clock.POSIX
import System.ZMQ4 hiding (Event(..)) import Database.Redis hiding (decode, info)
import GHC.Generics
import Options.Applicative
import System.Exit
import System.IO
import System.Log.Formatter
import System.Log.Handler (setFormatter)
import System.Log.Handler.Simple
import System.Log.Logger
import System.Signal
import System.ZMQ4 hiding (Event (..))
import Ether.Reader import Ether.Reader
@ -106,18 +113,18 @@ instance (MonadRobot (RealDriver c s) c s) where
getEnvironment = asks @RDriverEnv environmentRef >>= liftIO . readIORef getEnvironment = asks @RDriverEnv environmentRef >>= liftIO . readIORef
data Params = Params { data Params = Params {
instanceId :: String, instanceId :: String,
strategyConfigFile :: FilePath, strategyConfigFile :: FilePath,
strategyStateFile :: FilePath, strategyStateFile :: FilePath,
brokerEp :: String, brokerEp :: String,
quotesourceEp :: String, quotesourceEp :: String,
historyProviderType :: Maybe String, historyProviderType :: Maybe String,
historyProvider :: Maybe String, historyProvider :: Maybe String,
redisSocket :: Maybe String, redisSocket :: Maybe String,
qtisSocket :: Maybe String, qtisEndpoint :: String,
accountId :: String, accountId :: String,
volumeFactor :: Int, volumeFactor :: Int,
sourceBarTimeframe :: Maybe Int sourceBarTimeframe :: Maybe Int
} deriving (Show, Eq) } deriving (Show, Eq)
paramsParser :: Parser Params paramsParser :: Parser Params
@ -146,9 +153,9 @@ paramsParser = Params
<*> optional ( strOption <*> optional ( strOption
( long "redis-socket" ( long "redis-socket"
<> metavar "ADDRESS" )) <> metavar "ADDRESS" ))
<*> optional ( strOption <*> strOption
( long "qtis" ( long "qtis"
<> metavar "ENDPOINT/ID" )) <> metavar "ENDPOINT/ID" )
<*> strOption <*> strOption
( long "account" ( long "account"
<> metavar "ACCOUNT" ) <> metavar "ACCOUNT" )
@ -159,9 +166,79 @@ paramsParser = Params
( long "source-timeframe" ( long "source-timeframe"
<> metavar "SECONDS" )) <> metavar "SECONDS" ))
data Env historySource c s = Env {
envZeromqContext :: Context,
envHistorySource :: historySource,
envQtisEndpoint :: T.Text,
envStrategyInstanceParams :: StrategyInstanceParams,
envStrategyEnvironment :: IORef StrategyEnvironment,
envConfigRef :: IORef c,
envStateRef :: IORef s,
envBrokerChan :: BC.BoundedChan BrokerCommand,
envTimers :: IORef [UTCTime],
envEventChan :: BC.BoundedChan Event,
envAggregator :: IORef BarAggregator,
envLastTimestamp :: IORef UTCTime
} deriving (Generic)
type App historySource c s = ReaderT (Env historySource c s) IO
instance MonadRobot (App historySource c s) c s where
submitOrder order = do
bc <- asks envBrokerChan
lift $ BC.writeChan bc $ BrokerSubmitOrder order
cancelOrder oId = do
bc <- asks envBrokerChan
lift $ BC.writeChan bc $ BrokerCancelOrder oId
appendToLog = lift . debugM "Strategy" . T.unpack . TL.toStrict
setupTimer t = do
timers <- asks envTimers
lift $ atomicModifyIORef' timers (\s -> (t : s, ()))
enqueueIOAction actionId action' = do
eventChan <- asks envEventChan
lift $ void $ forkIO $ do
v <- action'
BC.writeChan eventChan $ ActionCompleted actionId v
getConfig = asks envConfigRef >>= lift . readIORef
getState = asks envStateRef >>= lift . readIORef
setState s = do
ref <- asks envStateRef
lift $ writeIORef ref s
getEnvironment = do
aggRef <- asks envAggregator
envRef <- asks envStrategyEnvironment
agg <- lift $ readIORef aggRef
env <- lift $ readIORef envRef
nowRef <- asks envLastTimestamp
now <- lift $ readIORef nowRef
return $ env & seBars .~ bars agg & seLastTimestamp .~ now
instance MonadHistory (App QQ.QHPHandle c s) where
getHistory tickerId timeframe fromTime toTime = do
qhp <- asks envHistorySource
QQ.requestHistoryFromQHP qhp tickerId timeframe fromTime toTime
instance MonadInstrumentParametersSource (App hs c s) where
getInstrumentParameters tickerIds = do
ctx <- asks envZeromqContext
ep <- asks envQtisEndpoint
info <- liftIO $ qtisGetTickersInfo ctx ep tickerIds
return $ (tiTicker info, convert info)
where
convert info = InstrumentParameters
{
ipLotSize = fromInteger $ tiLotSize info,
ipTickSize = tiTickSize info
}
data BigConfig c = BigConfig { data BigConfig c = BigConfig {
confTickers :: [Ticker], confTickers :: [Ticker],
strategyConfig :: c strategyConfig :: c
} }
@ -182,7 +259,7 @@ storeState params stateRef timersRef = do
Nothing -> withFile (strategyStateFile params) WriteMode (\f -> BS.hPut f $ BL.toStrict $ encode currentStrategyState) Nothing -> withFile (strategyStateFile params) WriteMode (\f -> BS.hPut f $ BL.toStrict $ encode currentStrategyState)
`catch` (\e -> warningM "main" ("Unable to save state: " ++ show (e :: IOException))) `catch` (\e -> warningM "main" ("Unable to save state: " ++ show (e :: IOException)))
Just sock -> do Just sock -> do
#ifdef linux_HOST_OS
conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock } conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock }
now <- getPOSIXTime now <- getPOSIXTime
res <- runRedis conn $ mset [(encodeUtf8 $ T.pack $ instanceId params, BL.toStrict $ encode currentStrategyState), res <- runRedis conn $ mset [(encodeUtf8 $ T.pack $ instanceId params, BL.toStrict $ encode currentStrategyState),
@ -190,12 +267,8 @@ storeState params stateRef timersRef = do
(encodeUtf8 $ T.pack $ instanceId params ++ ":timers", BL.toStrict $ encode currentTimersState) ] (encodeUtf8 $ T.pack $ instanceId params ++ ":timers", BL.toStrict $ encode currentTimersState) ]
case res of case res of
Left _ -> warningM "main" "Unable to save state" Left _ -> warningM "main" "Unable to save state"
Right _ -> return () Right _ -> return ()
#else
return ()
#endif
gracefulShutdown :: (ToJSON s) => Params -> IORef s -> IORef [UTCTime] -> MVar () -> Signal -> IO () gracefulShutdown :: (ToJSON s) => Params -> IORef s -> IORef [UTCTime] -> MVar () -> Signal -> IO ()
gracefulShutdown params stateRef timersRef shutdownMv _ = do gracefulShutdown params stateRef timersRef shutdownMv _ = do
@ -204,8 +277,8 @@ gracefulShutdown params stateRef timersRef shutdownMv _ = do
putMVar shutdownMv () putMVar shutdownMv ()
exitSuccess exitSuccess
robotMain :: (ToJSON s, FromJSON s, FromJSON c) => DiffTime -> s -> Maybe (InitializationCallback c) -> EventCallback c s -> IO () robotMain :: (ToJSON s, FromJSON s, FromJSON c) => DiffTime -> s -> EventCallback c s -> IO ()
robotMain dataDownloadDelta defaultState initCallback callback = do robotMain dataDownloadDelta defaultState callback = do
params <- execParser opts params <- execParser opts
initLogging params initLogging params
infoM "main" "Starting" infoM "main" "Starting"
@ -219,43 +292,70 @@ robotMain dataDownloadDelta defaultState initCallback callback = do
strategyAccount = T.pack . accountId $ params, strategyAccount = T.pack . accountId $ params,
strategyVolume = volumeFactor params, strategyVolume = volumeFactor params,
tickers = tickerList, tickers = tickerList,
strategyQuotesourceEp = T.pack . quotesourceEp $ params, strategyQTISEp = Nothing }
strategyBrokerEp = T.pack . brokerEp $ params,
strategyHistoryProviderType = T.pack $ fromMaybe "finam" $ historyProviderType params,
strategyHistoryProvider = T.pack $ fromMaybe "" $ historyProvider params,
strategyQTISEp = T.pack <$> qtisSocket params}
updatedConfig <- case initCallback of
Just cb -> cb config instanceParams
Nothing -> return config
let strategy = mkBarStrategy instanceParams dataDownloadDelta updatedConfig stratState callback
stateRef <- newIORef stratState stateRef <- newIORef stratState
configRef <- newIORef config
timersRef <- newIORef timersState timersRef <- newIORef timersState
shutdownMv <- newEmptyMVar shutdownMv <- newEmptyMVar
installHandler sigINT (gracefulShutdown params stateRef timersRef shutdownMv) installHandler sigINT (gracefulShutdown params stateRef timersRef shutdownMv)
installHandler sigTERM (gracefulShutdown params stateRef timersRef shutdownMv) installHandler sigTERM (gracefulShutdown params stateRef timersRef shutdownMv)
randsec <- getStdRandom(randomR(1, 10))
threadDelay $ randsec * 1000000
debugM "main" "Forking state saving thread" debugM "main" "Forking state saving thread"
stateSavingThread <- forkIO $ forever $ do stateSavingThread <- forkIO $ forever $ do
threadDelay 1000000 threadDelay 1000000
storeState params stateRef timersRef storeState params stateRef timersRef
straEnv <- newIORef StrategyEnvironment {
_seInstanceId = strategyInstanceId instanceParams,
_seAccount = strategyAccount instanceParams,
_seVolume = strategyVolume instanceParams,
_seBars = M.empty,
_seLastTimestamp = UTCTime (fromGregorian 1970 1 1) 0
}
-- Event channel is for strategy events, like new tick arrival, or order execution notification
eventChan <- BC.newBoundedChan 1000
-- Orders channel passes strategy orders to broker thread
brokerChan <- BC.newBoundedChan 1000
debugM "main" "Starting strategy driver" debugM "main" "Starting strategy driver"
barStrategyDriver (sourceBarTimeframe params) tickFilter strategy stateRef timersRef shutdownMv `finally` killThread stateSavingThread withContext (\ctx -> do
let qsEp = T.pack $ quotesourceEp params
let brEp = T.pack $ brokerEp params
agg <- newIORef $ mkAggregatorFromBars M.empty [(hmsToDiffTime 3 50 0, hmsToDiffTime 21 10 0)]
bracket (startQuoteSourceThread ctx qsEp instanceParams eventChan agg tickFilter (sourceBarTimeframe params)) killThread $ \_ -> do
debugM "Strategy" "QuoteSource thread forked"
bracket (startBrokerClientThread (strategyInstanceId instanceParams) ctx brEp brokerChan eventChan shutdownMv) killThread $ \_ -> do
debugM "Strategy" "Broker thread forked"
now <- getCurrentTime >>= newIORef
let env = Env {
envZeromqContext = ctx,
envQtisEndpoint = T.pack . qtisEndpoint $ params,
envHistorySource = mkQHPHandle ctx (T.pack . fromMaybe "" . historyProvider $ params),
envStrategyInstanceParams = instanceParams,
envStrategyEnvironment = straEnv,
envConfigRef = configRef,
envStateRef = stateRef,
envBrokerChan = brokerChan,
envTimers = timersRef,
envEventChan = eventChan,
envAggregator = agg,
envLastTimestamp = now
}
runReaderT (barStrategyDriver dataDownloadDelta instanceParams callback shutdownMv) env `finally` killThread stateSavingThread)
where where
tickFilter :: Tick -> Bool tickFilter :: Tick -> Bool
tickFilter tick = tickFilter tick =
let classCode = T.takeWhile (/= '#') (security tick) in let classCode = T.takeWhile (/= '#') (security tick) in
if if classCode == "SPBFUT" || classCode == "SPBOPT"
| classCode == "SPBFUT" || classCode == "SPBOPT" -> any (inInterval . utctDayTime . timestamp $ tick) fortsIntervals then any (inInterval . utctDayTime . timestamp $ tick) fortsIntervals
| otherwise -> any (inInterval . utctDayTime . timestamp $ tick) secIntervals else any (inInterval . utctDayTime . timestamp $ tick) secIntervals
fortsIntervals = [(fromHMS 7 0 0, fromHMS 11 0 0), (fromHMS 11 5 0, fromHMS 15 45 0), (fromHMS 16 0 0, fromHMS 20 50 0)] fortsIntervals = [(fromHMS 4 0 0, fromHMS 11 0 0), (fromHMS 11 5 0, fromHMS 15 45 0), (fromHMS 16 0 0, fromHMS 20 50 0)]
secIntervals = [(fromHMS 6 50 0, fromHMS 15 51 0)] secIntervals = [(fromHMS 6 50 0, fromHMS 15 51 0)]
fromHMS h m s = h * 3600 + m * 60 + s
inInterval ts (start, end) = ts >= start && ts <= end inInterval ts (start, end) = ts >= start && ts <= end
opts = info (helper <*> paramsParser) opts = info (helper <*> paramsParser)
@ -274,16 +374,16 @@ robotMain dataDownloadDelta defaultState initCallback callback = do
loadStrategyConfig params = withFile (strategyConfigFile params) ReadMode (\f -> do loadStrategyConfig params = withFile (strategyConfigFile params) ReadMode (\f -> do
bigconfig <- eitherDecode . BL.fromStrict <$> BS.hGetContents f bigconfig <- eitherDecode . BL.fromStrict <$> BS.hGetContents f
case bigconfig of case bigconfig of
Right conf -> return (confTickers conf, strategyConfig conf) Right conf -> return (confTickers conf, strategyConfig conf)
Left errmsg -> throw $ UnableToLoadConfig $ (T.pack . show) errmsg) Left errmsg -> throw $ UnableToLoadConfig $ (T.pack . show) errmsg)
loadStrategyTimers :: Params -> IO [UTCTime] loadStrategyTimers :: Params -> IO [UTCTime]
loadStrategyTimers params = case redisSocket params of loadStrategyTimers params = case redisSocket params of
Nothing -> return [] Nothing -> return []
Just sock -> do Just sock -> do
#ifdef linux_HOST_OS
conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock } conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock }
res <- runRedis conn $ get (encodeUtf8 $ T.pack $ instanceId params ++ "timers") res <- runRedis conn $ get (encodeUtf8 $ T.pack $ instanceId params ++ ":timers")
case res of case res of
Left _ -> do Left _ -> do
warningM "main" "Unable to load state" warningM "main" "Unable to load state"
@ -297,15 +397,11 @@ robotMain dataDownloadDelta defaultState initCallback callback = do
Nothing -> do Nothing -> do
warningM "main" "Unable to load state" warningM "main" "Unable to load state"
return [] return []
#else
error "Not implemented"
#endif
loadStrategyState params = case redisSocket params of loadStrategyState params = case redisSocket params of
Nothing -> loadStateFromFile (strategyStateFile params) Nothing -> loadStateFromFile (strategyStateFile params)
Just sock -> do Just sock -> do
#ifdef linux_HOST_OS
conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock } conn <- checkedConnect $ defaultConnectInfo { connectPort = UnixSocket sock }
res <- runRedis conn $ get (encodeUtf8 $ T.pack $ instanceId params) res <- runRedis conn $ get (encodeUtf8 $ T.pack $ instanceId params)
case res of case res of
@ -321,10 +417,7 @@ robotMain dataDownloadDelta defaultState initCallback callback = do
Nothing -> do Nothing -> do
warningM "main" "Unable to load state" warningM "main" "Unable to load state"
return defaultState return defaultState
#else
error "Not implemented"
#endif
loadStateFromFile filepath = withFile filepath ReadMode (\f -> do loadStateFromFile filepath = withFile filepath ReadMode (\f -> do
maybeState <- decode . BL.fromStrict <$> BS.hGetContents f maybeState <- decode . BL.fromStrict <$> BS.hGetContents f
case maybeState of case maybeState of
@ -332,43 +425,27 @@ robotMain dataDownloadDelta defaultState initCallback callback = do
Nothing -> return defaultState ) `catch` Nothing -> return defaultState ) `catch`
(\e -> warningM "main" ("Unable to load state: " ++ show (e :: IOException)) >> return defaultState) (\e -> warningM "main" ("Unable to load state: " ++ show (e :: IOException)) >> return defaultState)
-- | Helper function to make 'Strategy' instances
mkBarStrategy :: StrategyInstanceParams -> DiffTime -> c -> s -> EventCallback c s -> Strategy c s
mkBarStrategy instanceParams dd params initialState cb = BarStrategy {
downloadDelta = dd,
eventCallback = cb,
currentState = initialState,
strategyParams = params,
strategyTimers = [],
strategyInstanceParams = instanceParams }
-- | Main function which handles incoming events (ticks/orders), passes them to strategy callback -- | Main function which handles incoming events (ticks/orders), passes them to strategy callback
-- and executes returned strategy actions -- and executes returned strategy actions
barStrategyDriver :: Maybe Int -> (Tick -> Bool) -> Strategy c s -> IORef s -> IORef [UTCTime] -> MVar () -> IO () barStrategyDriver :: (MonadHistory (App hs c s)) => DiffTime -> StrategyInstanceParams -> EventCallback c s -> MVar () -> App hs c s ()
barStrategyDriver mbSourceTimeframe tickFilter strategy stateRef timersRef shutdownVar = do barStrategyDriver downloadDelta instanceParams callback shutdownVar = do
-- Make channels now <- liftIO getCurrentTime
-- Event channel is for strategy events, like new tick arrival, or order execution notification history <- M.fromList <$> mapM (loadTickerHistory now) (tickers instanceParams)
eventChan <- BC.newBoundedChan 1000 eventChan <- asks envEventChan
-- Orders channel passes strategy orders to broker thread brokerChan <- asks envBrokerChan
ordersChan <- BC.newBoundedChan 1000 agg <- asks envAggregator
liftIO $ atomicModifyIORef' agg (\s -> (replaceHistory s history, ()))
withContext (\ctx -> do
-- Load tickers data and create BarAggregator from them wakeupTid <- lift . forkIO $ forever $ do
historyBars <- maybeShutdown <- tryTakeMVar shutdownVar
if if isJust maybeShutdown
| (strategyHistoryProviderType . strategyInstanceParams) strategy == "finam" -> then writeChan eventChan Shutdown
M.fromList <$> mapM loadTickerFromFinam (tickers . strategyInstanceParams $ strategy) else do
| (strategyHistoryProviderType . strategyInstanceParams) strategy == "hap" -> threadDelay 1000000
M.fromList <$> mapM (loadTickerFromHAP ctx ((strategyHistoryProvider . strategyInstanceParams) strategy)) (tickers . strategyInstanceParams $ strategy) writeChan brokerChan BrokerRequestNotifications
| otherwise -> lift $ debugM "Strategy" "Wakeup thread forked"
M.fromList <$> mapM (loadTickerFromQHP ctx ((strategyHistoryProvider . strategyInstanceParams) strategy)) (tickers . strategyInstanceParams $ strategy)
agg <- newIORef $ mkAggregatorFromBars historyBars [(hmsToDiffTime 6 50 0, hmsToDiffTime 21 0 0)] <<<<<<< HEAD
bracket (startQuoteSourceThread ctx qsEp strategy eventChan agg tickFilter mbSourceTimeframe) killThread (\_ -> do
debugM "Strategy" "QuoteSource thread forked"
bracket (startBrokerClientThread (strategyInstanceId . strategyInstanceParams $ strategy) ctx brEp ordersChan eventChan shutdownVar) killThread (\_ -> do
debugM "Strategy" "Broker thread forked"
wakeupTid <- forkIO $ forever $ do wakeupTid <- forkIO $ forever $ do
maybeShutdown <- tryTakeMVar shutdownVar maybeShutdown <- tryTakeMVar shutdownVar
if isJust maybeShutdown if isJust maybeShutdown
@ -407,10 +484,33 @@ barStrategyDriver mbSourceTimeframe tickFilter strategy stateRef timersRef shutd
env <- readIORef envRef env <- readIORef envRef
let oldTimestamp = seLastTimestamp env let oldTimestamp = seLastTimestamp env
=======
readAndHandleEvents agg instanceParams
lift $ debugM "Strategy" "Stopping strategy driver"
lift $ killThread wakeupTid
where
loadTickerHistory now t = do
history <- getHistory (code t) (BarTimeframe (fromInteger . timeframeSeconds $ t))
((fromRational . toRational . negate $ downloadDelta) `addUTCTime` now) now
instrumentParams <- snd <$> getInstrumentParameters (code t)
return (code t, BarSeries (code t) (Timeframe (timeframeSeconds t)) history instrumentParams)
readAndHandleEvents agg instanceParams' = do
eventChan <- asks envEventChan
event <- lift $ readChan eventChan
if event /= Shutdown
then do
env <- getEnvironment
>>>>>>> stable
let newTimestamp = case event of let newTimestamp = case event of
NewTick tick -> timestamp tick NewTick tick -> timestamp tick
_ -> seLastTimestamp env NewBar bar -> barTimestamp bar
_ -> env ^. seLastTimestamp
nowRef <- asks envLastTimestamp
lift $ writeIORef nowRef newTimestamp
<<<<<<< HEAD
newTimers <- catMaybes <$> (readIORef timersRef >>= mapM (checkTimer eventChan newTimestamp)) newTimers <- catMaybes <$> (readIORef timersRef >>= mapM (checkTimer eventChan newTimestamp))
atomicWriteIORef timersRef newTimers atomicWriteIORef timersRef newTimers
@ -419,117 +519,24 @@ barStrategyDriver mbSourceTimeframe tickFilter strategy stateRef timersRef shutd
readAndHandleEvents agg ordersChan eventChan strategy' envRef readAndHandleEvents agg ordersChan eventChan strategy' envRef
else debugM "Strategy" "Shutdown requested" else debugM "Strategy" "Shutdown requested"
=======
timersRef <- asks envTimers
oldTimers <- lift $ readIORef timersRef
newTimers <- catMaybes <$> mapM (checkTimer eventChan newTimestamp) oldTimers
callback event
lift $ writeIORef timersRef newTimers
readAndHandleEvents agg instanceParams'
else
lift $ debugM "Strategy" "Shutdown requested"
>>>>>>> stable
where where
handleTimerActions action =
case action of
ActionSetupTimer timerTime -> return $ Just timerTime
_ -> return Nothing
handleActions ordersChan' action =
case action of
(ActionLog logText) -> debugM "Strategy" $ T.unpack logText
(ActionOrder order) -> writeChan ordersChan' $ BrokerSubmitOrder order
(ActionCancelOrder oid) -> writeChan ordersChan' $ BrokerCancelOrder oid
(ActionSetupTimer _) -> return ()
(ActionIO tag io) -> void $ forkIO $ do
v <- io
writeChan eventChan (ActionCompleted tag v)
checkTimer eventChan' newTimestamp timerTime = checkTimer eventChan' newTimestamp timerTime =
if newTimestamp >= timerTime if newTimestamp >= timerTime
then do then do
writeChan eventChan' $ TimerFired timerTime lift $ writeChan eventChan' $ TimerFired timerTime
return Nothing return Nothing
else else
return $ Just timerTime return $ Just timerTime
loadTickerFromHAP :: Context -> T.Text -> Ticker -> IO (TickerId, BarSeries)
loadTickerFromHAP ctx ep t = do
debugM "Strategy" $ "Loading ticker from HAP: " ++ show (code t)
case parseHAPPeriod $ timeframeSeconds t of
Just tf -> do
now <- getCurrentTime
historyBars <- QH.getQuotes ctx QH.RequestParams {
QH.endpoint = ep,
QH.ticker = code t,
QH.startDate = addUTCTime (negate . (1 +) . fromRational . toRational $ downloadDelta strategy) now,
QH.endDate = now,
QH.period = tf }
debugM "Strategy" $ "Obtained " ++ show (length historyBars) ++ " bars"
return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = historyBars })
_ -> return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = [] })
loadTickerFromQHP :: Context -> T.Text -> Ticker -> IO (TickerId, BarSeries)
loadTickerFromQHP ctx ep t = do
debugM "Strategy" $ "Loading ticker from QHP: " ++ show (code t)
case parseQHPPeriod $ timeframeSeconds t of
Just tf -> do
now <- getCurrentTime
historyBars <- QQ.getQuotes ctx QQ.RequestParams {
QQ.endpoint = ep,
QQ.ticker = code t,
QQ.startDate = addDays (negate . (1 +) . ceiling $ downloadDelta strategy / 86400) (utctDay now),
QQ.endDate = utctDay now,
QQ.period = tf }
debugM "Strategy" $ "Obtained " ++ show (length historyBars) ++ " bars"
return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = historyBars })
_ -> return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = [] })
loadTickerFromFinam :: Ticker -> IO (TickerId, BarSeries)
loadTickerFromFinam t = do
randDelay <- getStdRandom (randomR (1, 5))
threadDelay $ randDelay * 1000000
now <- getCurrentTime
debugM "Strategy" $ show (L.lookup "finam" (aliases t), parseFinamPeriod $ timeframeSeconds t)
case (L.lookup "finam" (aliases t), parseFinamPeriod $ timeframeSeconds t) of
(Just finamCode, Just per) -> do
debugM "Strategy" $ "Downloading ticker: " ++ finamCode
history <- downloadAndParseQuotes $ defaultParams { QF.ticker = T.pack finamCode,
QF.startDate = addDays (negate . (1 +) . ceiling $ downloadDelta strategy / 86400) (utctDay now),
QF.endDate = utctDay now,
QF.period = per }
case history of
Just h -> return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = convertFromFinamHistory (code t) h })
Nothing -> return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = [] })
_ -> return (code t, BarSeries { bsTickerId = code t, bsTimeframe = Timeframe (timeframeSeconds t), bsBars = [] })
convertFromFinamHistory :: TickerId -> [Row] -> [Bar]
convertFromFinamHistory tid = L.reverse . fmap (\row -> Bar { barSecurity = tid,
barTimestamp = rowTime row,
barOpen = rowOpen row,
barHigh = rowHigh row,
barLow = rowLow row,
barClose = rowClose row,
barVolume = rowVolume row })
parseFinamPeriod x
| x == 0 = Just QF.PeriodTick
| x == 60 = Just QF.Period1Min
| x == 5 * 60 = Just QF.Period5Min
| x == 10 * 60 = Just QF.Period10Min
| x == 15 * 60 = Just QF.Period15Min
| x == 30 * 60 = Just QF.Period30Min
| x == 60 * 60 = Just QF.PeriodHour
| x == 24 * 60 * 60 = Just QF.PeriodDay
| otherwise = Nothing
parseQHPPeriod x
| x == 60 = Just QQ.Period1Min
| x == 5 * 60 = Just QQ.Period5Min
| x == 15 * 60 = Just QQ.Period15Min
| x == 30 * 60 = Just QQ.Period30Min
| x == 60 * 60 = Just QQ.PeriodHour
| x == 24 * 60 * 60 = Just QQ.PeriodDay
| otherwise = Nothing
parseHAPPeriod x
| x == 60 = Just QH.Period1Min
| x == 5 * 60 = Just QH.Period5Min
| x == 15 * 60 = Just QH.Period15Min
| x == 30 * 60 = Just QH.Period30Min
| x == 60 * 60 = Just QH.PeriodHour
| x == 24 * 60 * 60 = Just QH.PeriodDay
| otherwise = Nothing

95
src/ATrade/Driver/Real/BrokerClientThread.hs

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module ATrade.Driver.Real.BrokerClientThread ( module ATrade.Driver.Real.BrokerClientThread (
startBrokerClientThread, startBrokerClientThread,
@ -9,7 +10,6 @@ import ATrade.Broker.Client
import ATrade.Broker.Protocol import ATrade.Broker.Protocol
import ATrade.RoboCom.Monad hiding (cancelOrder, import ATrade.RoboCom.Monad hiding (cancelOrder,
submitOrder) submitOrder)
import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Control.Concurrent hiding (readChan, writeChan, import Control.Concurrent hiding (readChan, writeChan,
@ -28,47 +28,58 @@ import Data.Time.Clock
import System.Log.Logger import System.Log.Logger
import System.ZMQ4 hiding (Event) import System.ZMQ4 hiding (Event)
data BrokerCommand = BrokerSubmitOrder Order | BrokerCancelOrder Integer | BrokerRequestNotifications data BrokerCommand = BrokerSubmitOrder Order | BrokerCancelOrder Integer | BrokerRequestNotifications | BrokerHandleNotification Notification
startBrokerClientThread :: T.Text -> Context -> T.Text -> T.Text -> BoundedChan BrokerCommand -> BoundedChan Event -> MVar a -> IO ThreadId
startBrokerClientThread :: T.Text -> Context -> T.Text -> BoundedChan BrokerCommand -> BoundedChan Event -> MVar a -> IO ThreadId startBrokerClientThread instId ctx brEp notifEp ordersChan eventChan shutdownVar = do
startBrokerClientThread instId ctx brEp ordersChan eventChan shutdownVar = forkIO $ whileM_ (isNothing <$> tryReadMVar shutdownVar) $ let callback = writeChan ordersChan . BrokerHandleNotification
bracket (startBrokerClient (encodeUtf8 instId) ctx brEp defaultClientSecurityParams) forkIO $ whileM_ (isNothing <$> tryReadMVar shutdownVar) $
(\bro -> do bracket (startBrokerClient (encodeUtf8 instId) ctx brEp notifEp [callback] defaultClientSecurityParams)
stopBrokerClient bro (\bro -> do
debugM "Strategy" "Broker client: stop") stopBrokerClient bro
(\bs -> handle (\e -> do debugM "Strategy" "Broker client: stop")
warningM "Strategy" $ "Broker client: exception: " ++ show (e :: SomeException) (\bs -> handle (\e -> do
throwIO e) $ do warningM "Strategy" $ "Broker client: exception: " ++ show (e :: SomeException)
now <- getCurrentTime throwIO e) $ do
lastNotificationTime <- newIORef now now <- getCurrentTime
whileM_ (andM [notTimeout lastNotificationTime, isNothing <$> tryReadMVar shutdownVar]) $ do lastNotificationTime <- newIORef now
brokerCommand <- readChan ordersChan lastKnownSqnum <- newIORef 0
case brokerCommand of whileM_ (andM [notTimeout lastNotificationTime, isNothing <$> tryReadMVar shutdownVar]) $ do
BrokerSubmitOrder order -> do brokerCommand <- readChan ordersChan
debugM "Strategy" $ "Submitting order: " ++ show order case brokerCommand of
maybeOid <- submitOrder bs order BrokerSubmitOrder order -> do
debugM "Strategy" "Order submitted" debugM "Strategy" $ "Submitting order: " ++ show order
case maybeOid of result <- submitOrder bs order
Right oid -> writeChan eventChan (OrderSubmitted order { orderId = oid }) debugM "Strategy" "Order submitted"
Left errmsg -> debugM "Strategy" $ T.unpack $ "Error: " `T.append` errmsg case result of
BrokerCancelOrder oid -> do Right _ -> debugM "Strategy" $ "Order submitted: " ++ show (orderId order)
debugM "Strategy" $ "Cancelling order: " ++ show oid
_ <- cancelOrder bs oid
debugM "Strategy" "Order cancelled"
BrokerRequestNotifications -> do
t <- getCurrentTime
nt <- readIORef lastNotificationTime
when (t `diffUTCTime` nt > 1) $ do
maybeNs <- getNotifications bs
case maybeNs of
Left errmsg -> debugM "Strategy" $ T.unpack $ "Error: " `T.append` errmsg Left errmsg -> debugM "Strategy" $ T.unpack $ "Error: " `T.append` errmsg
Right ns -> do BrokerCancelOrder oid -> do
mapM_ (sendNotification eventChan) ns debugM "Strategy" $ "Cancelling order: " ++ show oid
getCurrentTime >>= (writeIORef lastNotificationTime) _ <- cancelOrder bs oid
nTimeout <- notTimeout lastNotificationTime debugM "Strategy" "Order cancelled"
shouldShutdown <- isNothing <$> tryReadMVar shutdownVar BrokerRequestNotifications -> do
debugM "Strategy" $ "Broker loop end: " ++ show nTimeout ++ "/" ++ show shouldShutdown) t <- getCurrentTime
nt <- readIORef lastNotificationTime
when (t `diffUTCTime` nt > 1) $ do
maybeNs <- getNotifications bs
case maybeNs of
Left errmsg -> debugM "Strategy" $ T.unpack $ "Error: " `T.append` errmsg
Right ns -> do
mapM_ (\n -> do
prevSqnum <- atomicModifyIORef lastKnownSqnum (\s -> (getNotificationSqnum n, s))
when (prevSqnum + 1 < getNotificationSqnum n) $
warningM "Strategy" $ "Sqnum jump: " ++ show prevSqnum ++ "->" ++ show (getNotificationSqnum n)
sendNotification eventChan n) ns
getCurrentTime >>= writeIORef lastNotificationTime
BrokerHandleNotification notification -> do
sendNotification eventChan n
prevSqnum <- atomicModifyIORef lastKnownSqnum (\s -> (getNotificationSqnum n, s))
undefined
nTimeout <- notTimeout lastNotificationTime
shouldShutdown <- isNothing <$> tryReadMVar shutdownVar
debugM "Strategy" $ "Broker loop end: " ++ show nTimeout ++ "/" ++ show shouldShutdown)
notTimeout :: IORef UTCTime -> IO Bool notTimeout :: IORef UTCTime -> IO Bool
notTimeout ts = do notTimeout ts = do
@ -79,5 +90,5 @@ notTimeout ts = do
sendNotification :: BoundedChan Event -> Notification -> IO () sendNotification :: BoundedChan Event -> Notification -> IO ()
sendNotification eventChan notification = sendNotification eventChan notification =
writeChan eventChan $ case notification of writeChan eventChan $ case notification of
OrderNotification oid state -> OrderUpdate oid state OrderNotification sqnum oid state -> OrderUpdate oid state
TradeNotification trade -> NewTrade trade TradeNotification sqnum trade -> NewTrade trade

38
src/ATrade/Driver/Real/QuoteSourceThread.hs

@ -6,14 +6,13 @@ module ATrade.Driver.Real.QuoteSourceThread
) where ) where
import ATrade.BarAggregator import ATrade.BarAggregator
import ATrade.Driver.Real.Types import ATrade.Driver.Types
import ATrade.QuoteSource.Client import ATrade.QuoteSource.Client
import ATrade.RoboCom.Monad import ATrade.RoboCom.Monad
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Data.IORef import Data.IORef
import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import Control.Concurrent hiding (readChan, writeChan, import Control.Concurrent hiding (readChan, writeChan,
@ -25,32 +24,37 @@ import Control.Monad
import System.Log.Logger import System.Log.Logger
import System.ZMQ4 hiding (Event) import System.ZMQ4 hiding (Event)
startQuoteSourceThread :: Context -> T.Text -> Strategy c s -> BoundedChan Event -> IORef BarAggregator -> (Tick -> Bool) -> Maybe Int -> IO ThreadId startQuoteSourceThread :: Context -> T.Text -> StrategyInstanceParams -> BoundedChan Event -> IORef BarAggregator -> (Tick -> Bool) -> Maybe Int -> IO ThreadId
startQuoteSourceThread ctx qsEp strategy eventChan agg tickFilter maybeSourceTimeframe = forkIO $ do startQuoteSourceThread ctx qsEp instanceParams eventChan agg tickFilter maybeSourceTimeframe = forkIO $ do
tickChan <- newBoundedChan 1000 tickChan <- newBoundedChan 1000
bracket (startQuoteSourceClient tickChan tickersList ctx qsEp) bracket (startQuoteSourceClient tickChan tickersList ctx qsEp defaultClientSecurityParams)
(\qs -> do (\qs -> do
stopQuoteSourceClient qs stopQuoteSourceClient qs
debugM "Strategy" "Quotesource client: stop") debugM "QSThread" "Quotesource client: stop")
(\_ -> forever $ do (\_ -> forever $ do
qdata <- readChan tickChan qdata <- readChan tickChan
case qdata of case qdata of
QDTick tick -> when (goodTick tick) $ do QDTick tick -> when (goodTick tick) $ do
writeChan eventChan (NewTick tick) writeChan eventChan (NewTick tick)
when (isNothing maybeSourceTimeframe) $ do case maybeSourceTimeframe of
aggValue <- readIORef agg Nothing -> do
case handleTick tick aggValue of aggValue <- readIORef agg
(Just bar, !newAggValue) -> writeChan eventChan (NewBar bar) >> writeIORef agg newAggValue case handleTick tick aggValue of
(Nothing, !newAggValue) -> writeIORef agg newAggValue (Just bar, !newAggValue) -> writeIORef agg newAggValue >> writeChan eventChan (NewBar bar)
QDBar (_, bar) -> do (Nothing, !newAggValue) -> writeIORef agg newAggValue
Just _ -> return ()
QDBar (incomingTf, bar) -> do
aggValue <- readIORef agg aggValue <- readIORef agg
when (isJust maybeSourceTimeframe) $ do -- debugM "QSThread" $ "Incoming bar: " ++ show incomingTf ++ ": " ++ show bar
case handleBar bar aggValue of case maybeSourceTimeframe of
(Just bar', !newAggValue) -> writeChan eventChan (NewBar bar') >> writeIORef agg newAggValue Just tf -> when (tf == unBarTimeframe incomingTf) $
(Nothing, !newAggValue) -> writeIORef agg newAggValue) case handleBar bar aggValue of
(Just bar', !newAggValue) -> writeIORef agg newAggValue >> writeChan eventChan (NewBar bar')
(Nothing, !newAggValue) -> writeIORef agg newAggValue
_ -> return ())
where where
goodTick tick = tickFilter tick && goodTick tick = tickFilter tick &&
(datatype tick /= LastTradePrice || (datatype tick == LastTradePrice && volume tick > 0)) (datatype tick /= LastTradePrice || (datatype tick == LastTradePrice && volume tick > 0))
tickersList = fmap code . (tickers . strategyInstanceParams) $ strategy tickersList = fmap code . tickers $ instanceParams

39
src/ATrade/Driver/Real/Types.hs

@ -1,39 +0,0 @@
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Real.Types (
Strategy(..),
StrategyInstanceParams(..),
InitializationCallback
) where
import ATrade.RoboCom.Monad
import ATrade.RoboCom.Types
import qualified Data.Text as T
import Data.Time.Clock
-- | Top-level strategy configuration and state
data Strategy c s = BarStrategy {
downloadDelta :: DiffTime, -- ^ How much history to download at strategy start
eventCallback :: EventCallback c s, -- ^ Strategy event callback
currentState :: s, -- ^ Current strategy state. Updated after each 'EventCallback' call
strategyParams :: c, -- ^ Strategy params
strategyTimers :: [UTCTime],
strategyInstanceParams :: StrategyInstanceParams -- ^ Instance params
}
-- | Strategy instance params store few params which are common for all strategies
data StrategyInstanceParams = StrategyInstanceParams {
strategyInstanceId :: T.Text, -- ^ Strategy instance identifier. Should be unique among all strategies (very desirable)
strategyAccount :: T.Text, -- ^ Account string to use for this strategy instance. Broker-dependent
strategyVolume :: Int, -- ^ Volume to use for this instance (in lots/contracts)
tickers :: [Ticker], -- ^ List of tickers which is used by this strategy
strategyQuotesourceEp :: T.Text, -- ^ QuoteSource server endpoint
strategyBrokerEp :: T.Text, -- ^ Broker server endpoint
strategyHistoryProviderType :: T.Text,
strategyHistoryProvider :: T.Text,
strategyQTISEp :: Maybe T.Text
}
type InitializationCallback c = c -> StrategyInstanceParams -> IO c

22
src/ATrade/Driver/Types.hs

@ -0,0 +1,22 @@
{-# LANGUAGE RankNTypes #-}
module ATrade.Driver.Types
(
StrategyInstanceParams(..),
InitializationCallback
) where
import ATrade.RoboCom.Types
import qualified Data.Text as T
-- | Strategy instance params store few params which are common for all strategies
data StrategyInstanceParams = StrategyInstanceParams {
strategyInstanceId :: T.Text, -- ^ Strategy instance identifier. Should be unique among all strategies (very desirable)
strategyAccount :: T.Text, -- ^ Account string to use for this strategy instance. Broker-dependent
strategyVolume :: Int, -- ^ Volume to use for this instance (in lots/contracts)
tickers :: [Ticker], -- ^ List of tickers which is used by this strategy
strategyQTISEp :: Maybe T.Text
}
type InitializationCallback c = c -> StrategyInstanceParams -> IO c

7
src/ATrade/Exceptions.hs

@ -8,7 +8,12 @@ import Control.Exception
import qualified Data.Text as T import qualified Data.Text as T
import GHC.Generics import GHC.Generics
data RoboComException = UnableToLoadConfig T.Text | UnableToLoadFeed T.Text data RoboComException = UnableToLoadConfig T.Text
| UnableToLoadFeed T.Text
| UnableToLoadState T.Text
| UnableToSaveState T.Text
| BadParams T.Text
| QTISFailure T.Text
deriving (Show, Generic) deriving (Show, Generic)
instance Exception RoboComException instance Exception RoboComException

153
src/ATrade/Forums/Smartlab.hs

@ -1,153 +0,0 @@
{-# OPTIONS_GHC -Wno-type-defaults #-}
module ATrade.Forums.Smartlab (
NewsItem(..),
IndexItem(..),
getIndex,
getItem
) where
import qualified Data.ByteString.Lazy as BL
import qualified Data.List as L
import Data.Maybe
import qualified Data.Text as T
import Data.Text.Encoding
import Data.Time.Calendar
import Data.Time.Clock
import Network.HTTP.Simple
import Safe
import Text.HTML.TagSoup
import Text.Parsec
import Text.Parsec.Text
import Text.StringLike
import Debug.Trace
data NewsItem = NewsItem {
niUrl :: !T.Text,
niHeader :: !T.Text,
niText :: !T.Text,
niAuthor :: !T.Text,
niPubTime :: !UTCTime
} deriving (Show, Eq)
data IndexItem = IndexItem {
iiUrl :: !T.Text,
iiTitle :: !T.Text,
iiPubTime :: !UTCTime
} deriving (Show, Eq)
monthNames :: [T.Text]
monthNames = fmap T.pack ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"]
extractBetween :: StringLike str => String -> [Tag str] -> [Tag str]
extractBetween tagName = takeWhile (~/= closeTag) . dropWhile (~/= openTag)
where
openTag = "<" ++ tagName ++ ">"
closeTag = "</" ++ tagName ++ ">"
matchClass :: T.Text -> T.Text -> Tag T.Text -> Bool
matchClass _ className (TagOpen _ attrs) = case L.lookup (T.pack "class") attrs of
Just klass -> className `L.elem` T.words klass
Nothing -> False
matchClass _ _ _ = False
parseTimestamp :: T.Text -> Maybe UTCTime
parseTimestamp text = case parse timestampParser "" text of
Left _ -> Nothing
Right val -> Just val
where
timestampParser :: Parser UTCTime
timestampParser = do
spaces
day <- read <$> many1 digit
spaces
monthName <- T.pack <$> many1 letter
case L.elemIndex monthName monthNames of
Nothing -> fail "Can't parse month"
Just month -> do
spaces
year <- fromIntegral . read <$> many1 digit
_ <- char ','
spaces
hour <- fromIntegral . read <$> many1 digit
_ <- char ':'
minute <- fromIntegral . read <$> many1 digit
return $ UTCTime (fromGregorian year (month + 1) day) (hour * 3600 + minute * 60)
getItem :: IndexItem -> IO (Maybe NewsItem)
getItem indexItem = do
rq <- parseRequest $ T.unpack (iiUrl indexItem)
resp <- httpLBS rq
if getResponseStatusCode resp == 200
then return . parseItem . decodeUtf8 . BL.toStrict . getResponseBody $ resp
else return Nothing
where
parseItem rawHtml = case parseTimestamp timestamp of
Just itemPubtime -> Just NewsItem {
niUrl = iiUrl indexItem,
niHeader = itemHeader,
niText = itemText,
niAuthor = itemAuthor,
niPubTime = itemPubtime
}
Nothing -> Nothing
where
itemHeader = innerText .
extractBetween "span" .
extractBetween "h1" .
dropWhile (not . matchClass (T.pack "div") (T.pack "topic")) $ tags
itemText = innerText .
extractBetween "div" .
dropWhile (not . matchClass (T.pack "div") (T.pack "content")) .
dropWhile (~/= "<div id=content_box>") $ tags
itemAuthor = innerText .
extractBetween "li" .
dropWhile (not . matchClass (T.pack "li") (T.pack "author")) $ tags
timestamp = traceShowId $ innerText .
extractBetween "li" .
dropWhile (not . matchClass (T.pack "li") (T.pack "date")) $ tags
tags = parseTags rawHtml
getIndex :: T.Text -> Int -> IO ([IndexItem], Bool)
getIndex rootUrl pageNumber = do
rq <- parseRequest $ T.unpack $ makeUrl rootUrl pageNumber
resp <- httpLBS rq
return $ if getResponseStatusCode resp == 200
then parseIndex . decodeUtf8 . BL.toStrict . getResponseBody $ resp
else ([], False)
where
parseIndex :: T.Text -> ([IndexItem], Bool)
parseIndex x = (mapMaybe parseIndexEntry $ partitions (matchClass (T.pack "div") (T.pack "topic")) $ parseTags x, hasNextPage $ parseTags x)
parseIndexEntry :: [Tag T.Text] -> Maybe IndexItem
parseIndexEntry divTag = do
a <- headMay . dropWhile (~/= "<a>") $ divTag
let text = innerText . takeWhile (~/= "</a>") . dropWhile (~/= "<a>") $ divTag
case a of
TagOpen _ attr -> do
href <- L.lookup (T.pack "href") attr
ts <- parseTimestamp (innerText $ takeWhile (~/= "</li>") . dropWhile (not . matchClass (T.pack "li") (T.pack "date")) $ divTag)
Just IndexItem { iiUrl = href,
iiTitle = text,
iiPubTime = ts }
_ -> Nothing
makeUrl root pagenumber
| pagenumber == 0 || pagenumber == 1 = root
| otherwise = root `T.append` (T.pack "/page") `T.append` T.pack (show pagenumber)
hasNextPage tags = if pageNumber <= 1
then paginationLinksCount > 0
else paginationLinksCount > 1
where
paginationLinksCount = length . filter (~== "<a>") . extractBetween "p" . dropWhile (~/= "<div id=pagination>") $ tags

23
src/ATrade/Quotes.hs

@ -0,0 +1,23 @@
{- |
- Module : ATrade.Quotes
- Various historical price series management stuff
-}
module ATrade.Quotes
(
MonadHistory(..)
, MonadInstrumentParametersSource(..)
) where
import ATrade.RoboCom.Types (InstrumentParameters (..))
import ATrade.Types (Bar, BarTimeframe, TickerId)
import Data.Time.Clock (UTCTime)
class (Monad m) => MonadHistory m where
-- | 'getHistory tickerId timeframe fromTime toTime' should return requested timeframe between 'fromTime' and 'toTime'
getHistory :: TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar]
class (Monad m) => MonadInstrumentParametersSource m where
getInstrumentParameters :: TickerId -> m (TickerId, InstrumentParameters)

4
src/ATrade/Quotes/Finam.hs

@ -200,8 +200,8 @@ instance FromRecord Row where
high <- v .! 5 high <- v .! 5
low <- v .! 6 low <- v .! 6
close <- v .! 7 close <- v .! 7
volume <- v .! 8 vol <- v .! 8
return $ Row tkr dt open high low close volume return $ Row tkr dt open high low close vol
| otherwise = mzero | otherwise = mzero
where where
parseDt :: B.ByteString -> B.ByteString -> Parser UTCTime parseDt :: B.ByteString -> B.ByteString -> Parser UTCTime

115
src/ATrade/Quotes/HAP.hs

@ -1,115 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module ATrade.Quotes.HAP (
getQuotes,
Period(..),
RequestParams(..)
) where
import ATrade.Types
import Data.Aeson
import Data.Binary.Get
import Data.Binary.IEEE754
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.Clock.POSIX
import System.Log.Logger
import System.ZMQ4
data Period =
Period1Min |
Period5Min |
Period15Min |
Period30Min |
PeriodHour |
PeriodDay |
PeriodWeek |
PeriodMonth
deriving (Eq)
instance Show Period where
show Period1Min = "M1"
show Period5Min = "M5"
show Period15Min = "M15"
show Period30Min = "M30"
show PeriodHour = "H1"
show PeriodDay = "D"
show PeriodWeek = "W"
show PeriodMonth = "MN"
data RequestParams =
RequestParams
{
endpoint :: T.Text,
ticker :: T.Text,
startDate :: UTCTime,
endDate :: UTCTime,
period :: Period
} deriving (Show, Eq)
instance ToJSON RequestParams where
toJSON p = object [ "ticker" .= ticker p,
"from" .= startDate p,
"to" .= endDate p,
"timeframe" .= show (period p) ]
getQuotes :: Context -> RequestParams -> IO [Bar]
getQuotes ctx params =
withSocket ctx Req $ \sock -> do
debugM "HAP" $ "Connecting to ep: " ++ show (endpoint params)
connect sock $ (T.unpack . endpoint) params
send sock [] (BL.toStrict $ encode params { period = Period1Min})
response <- receiveMulti sock
case response of
[header, rest] -> if header == "OK"
then return $ reverse $ resampleBars (period params) $ parseBars (ticker params) $ BL.fromStrict rest
else return []
_ -> return []
where
resampleBars p bars@(firstBar:rest) = resampleBars' (periodToSec p) rest firstBar []
resampleBars' p (bar:bars) currentBar resampled = if barNumber p currentBar == barNumber p bar
then resampleBars' p bars (aggregate currentBar bar) resampled
else resampleBars' p bars bar (currentBar : resampled)
periodToSec Period1Min = 60
periodToSec Period5Min = 60 * 5
periodToSec Period15Min = 60 * 15
periodToSec Period30Min = 60 * 30
periodToSec PeriodHour = 60 * 60
periodToSec PeriodDay = 60 * 60 * 24
periodToSec PeriodWeek = 86400 * 7
barNumber sec bar = truncate (utcTimeToPOSIXSeconds (barTimestamp bar)) `div` sec
aggregate currentBar newBar = currentBar {
barHigh = max (barHigh currentBar) (barHigh newBar),
barLow = min (barLow currentBar) (barLow newBar),
barClose = barClose newBar,
barTimestamp = barTimestamp newBar
}
parseBars :: TickerId -> BL.ByteString -> [Bar]
parseBars tickerId input =
case runGetOrFail parseBar input of
Left _ -> []
Right (rest, _, bar) -> bar : parseBars tickerId rest
where
parseBar = do
rawTimestamp <- realToFrac <$> getWord64le
baropen <- getDoublele
barhigh <- getDoublele
barlow <- getDoublele
barclose <- getDoublele
barvolume <- getWord64le
return Bar
{
barSecurity = tickerId,
barTimestamp = posixSecondsToUTCTime rawTimestamp,
barOpen = fromDouble baropen,
barHigh = fromDouble barhigh,
barLow = fromDouble barlow,
barClose = fromDouble barclose,
barVolume = toInteger barvolume
}

56
src/ATrade/Quotes/QHP.hs

@ -1,19 +1,25 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module ATrade.Quotes.QHP ( module ATrade.Quotes.QHP (
getQuotes,
Period(..), Period(..),
RequestParams(..) RequestParams(..),
QHPHandle,
mkQHPHandle,
requestHistoryFromQHP
) where ) where
import ATrade.Exceptions
import ATrade.Types import ATrade.Types
import Control.Exception.Safe (MonadThrow, throw)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson import Data.Aeson
import Data.Binary.Get import Data.Binary.Get
import Data.Binary.IEEE754 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 Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Data.Time.Format
import System.Log.Logger import System.Log.Logger
import System.ZMQ4 import System.ZMQ4
@ -38,6 +44,39 @@ instance Show Period where
show PeriodWeek = "W" show PeriodWeek = "W"
show PeriodMonth = "MN" show PeriodMonth = "MN"
data QHPHandle = QHPHandle
{
qhpContext :: Context
, qhpEndpoint :: T.Text
}
mkQHPHandle :: Context -> T.Text -> QHPHandle
mkQHPHandle = QHPHandle
requestHistoryFromQHP :: (MonadThrow m, MonadIO m) => QHPHandle -> TickerId -> BarTimeframe -> UTCTime -> UTCTime -> m [Bar]
requestHistoryFromQHP qhp tickerId timeframe fromTime toTime =
case parseQHPPeriod (unBarTimeframe timeframe) of
Just tf -> liftIO $ getQuotes (qhpContext qhp) (params tf)
_ -> throw $ BadParams "QHP: Unable to parse timeframe"
where
params tf = RequestParams
{
endpoint = qhpEndpoint qhp,
ticker = tickerId,
startDate = utctDay fromTime,
endDate = utctDay toTime,
period = tf
}
parseQHPPeriod x
| x == 60 = Just Period1Min
| x == 5 * 60 = Just Period5Min
| x == 15 * 60 = Just Period15Min
| x == 30 * 60 = Just Period30Min
| x == 60 * 60 = Just PeriodHour
| x == 24 * 60 * 60 = Just PeriodDay
| otherwise = Nothing
data RequestParams = data RequestParams =
RequestParams RequestParams
{ {
@ -48,10 +87,13 @@ data RequestParams =
period :: Period period :: Period
} deriving (Show, Eq) } deriving (Show, Eq)
printDatetime :: UTCTime -> String
printDatetime = formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
instance ToJSON RequestParams where instance ToJSON RequestParams where
toJSON p = object [ "ticker" .= ticker p, toJSON p = object [ "ticker" .= ticker p,
"from" .= showGregorian (startDate p), "from" .= printDatetime (UTCTime (startDate p) 0),
"to" .= showGregorian (endDate p), "to" .= printDatetime (UTCTime (endDate p) 0),
"timeframe" .= show (period p) ] "timeframe" .= show (period p) ]
getQuotes :: Context -> RequestParams -> IO [Bar] getQuotes :: Context -> RequestParams -> IO [Bar]

39
src/ATrade/Quotes/QTIS.hs

@ -3,17 +3,16 @@
module ATrade.Quotes.QTIS module ATrade.Quotes.QTIS
( (
TickerInfo(..), TickerInfo(..),
qtisGetTickersInfo, qtisGetTickersInfo
qtisGetTickersInfo'
) where ) where
import ATrade.Exceptions
import ATrade.Types import ATrade.Types
import Control.Monad import Control.Exception.Safe
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 Data.Maybe import qualified Data.Text as T
import qualified Data.Text as T
import System.Log.Logger import System.Log.Logger
import System.ZMQ4 import System.ZMQ4
@ -35,23 +34,21 @@ instance ToJSON TickerInfo where
"lot_size" .= tiLotSize ti, "lot_size" .= tiLotSize ti,
"tick_size" .= tiTickSize ti ] "tick_size" .= tiTickSize ti ]
qtisGetTickersInfo' :: T.Text -> [TickerId] -> IO [TickerInfo] qtisGetTickersInfo :: Context -> T.Text -> TickerId -> IO TickerInfo
qtisGetTickersInfo' endpoint tickers = withContext (\ctx -> qtisGetTickersInfo ctx endpoint tickers) qtisGetTickersInfo ctx endpoint tickerId =
withSocket ctx Req $ \sock -> do
qtisGetTickersInfo :: Context -> T.Text -> [TickerId] -> IO [TickerInfo]
qtisGetTickersInfo ctx endpoint tickers =
withSocket ctx Req (\sock -> do
debugM "QTIS" $ "Connecting to: " ++ T.unpack endpoint debugM "QTIS" $ "Connecting to: " ++ T.unpack endpoint
connect sock $ T.unpack endpoint connect sock $ T.unpack endpoint
catMaybes <$> forM tickers (\tickerId -> do debugM "QTIS" $ "Requesting: " ++ T.unpack tickerId
debugM "QTIS" $ "Requesting: " ++ T.unpack tickerId send sock [] $ BL.toStrict tickerRequest
send sock [] $ BL.toStrict (tickerRequest tickerId) response <- receiveMulti sock
response <- receiveMulti sock let r = parseResponse response
let r = parseResponse response debugM "QTIS" $ "Got response: " ++ show r
debugM "QTIS" $ "Got response: " ++ show r case r of
return r)) Just resp -> return resp
Nothing -> throw $ QTISFailure "Can't parse response"
where where
tickerRequest tickerId = encode $ object ["ticker" .= tickerId] tickerRequest = encode $ object ["ticker" .= tickerId]
parseResponse :: [BC8.ByteString] -> Maybe TickerInfo parseResponse :: [BC8.ByteString] -> Maybe TickerInfo
parseResponse (header:payload:_) = if header == "OK" parseResponse (header:payload:_) = if header == "OK"
then decode $ BL.fromStrict payload then decode $ BL.fromStrict payload

27
src/ATrade/RoboCom.hs

@ -0,0 +1,27 @@
{-# LANGUAGE TemplateHaskell #-}
module ATrade.RoboCom
(
robocom_version
, robocom_gitrev
) where
import Data.Version
import Paths_robocom_zero
import Development.GitRev
robocom_version :: Version
robocom_version = version
robocom_gitrev :: String
robocom_gitrev = concat [ "robocom-zero-",
$(gitBranch),
"@",
$(gitHash),
dirty ]
where
dirty | $(gitDirty) = "+"
| otherwise = ""

6
src/ATrade/RoboCom/Indicators.hs

@ -40,7 +40,7 @@ cci period bars = (head tp - tpMean) / (0.015 * meanDev)
typicalPrice a b c = (a + b + c) / 3 typicalPrice a b c = (a + b + c) / 3
atr :: Int -> [Bar] -> Double atr :: Int -> [Bar] -> Double
atr period bars = case reverse (take (5 * period) trueranges) of atr period bars = case reverse (take (10 * period) trueranges) of
(firstValue:rest) -> foldl (\x y -> (x * (period' - 1) + y) / period') firstValue rest (firstValue:rest) -> foldl (\x y -> (x * (period' - 1) + y) / period') firstValue rest
_ -> 0 _ -> 0
where where
@ -121,6 +121,6 @@ bbandUpper period devs values = sma period values + devs * sigma
mean = sma period mean = sma period
percentRank :: Int -> [Double] -> Double percentRank :: Int -> [Double] -> Double
percentRank period values@(v:vs) = fromIntegral (length (filter (\x -> x < v) $ take period values)) / fromIntegral (length (take period values)) percentRank period values@(v:_) = fromIntegral (length (filter (\x -> x < v) $ take period values)) / fromIntegral (length (take period values))
percentRank period [] = 0 percentRank _ [] = 0

78
src/ATrade/RoboCom/Monad.hs

@ -9,38 +9,35 @@
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
module ATrade.RoboCom.Monad ( module ATrade.RoboCom.Monad (
RState,
RConfig,
RActions,
REnv,
StrategyEnvironment(..), StrategyEnvironment(..),
StrategyElement, seInstanceId,
runStrategyElement, seAccount,
seVolume,
seBars,
seLastTimestamp,
EventCallback, EventCallback,
Event(..), Event(..),
StrategyMonad,
StrategyAction(..),
tellAction,
MonadRobot(..), MonadRobot(..),
also, also,
t,
st st
) where ) where
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Ether import Control.Lens
import Data.Aeson.Types import Data.Aeson.Types
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Time.Clock import Data.Time.Clock
import Text.Printf.TH import Language.Haskell.Printf
import Language.Haskell.TH.Quote (QuasiQuoter)
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 () submitOrder :: Order -> m ()
cancelOrder :: OrderId -> m () cancelOrder :: OrderId -> m ()
appendToLog :: T.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
@ -52,18 +49,8 @@ class (Monad m) => MonadRobot m c s | m -> c, m -> s where
setState (f oldState) setState (f oldState)
getEnvironment :: m StrategyEnvironment getEnvironment :: m StrategyEnvironment
data RState st :: QuasiQuoter
data RConfig st = t
data RActions
data REnv
type StrategyMonad c s = WriterT RActions [StrategyAction] (StateT RState s (ReaderT REnv StrategyEnvironment (Reader RConfig c)))
type StrategyElement c s r = (StrategyMonad c s) r
runStrategyElement :: c -> s -> StrategyEnvironment -> StrategyElement c s r -> (s, [StrategyAction], r)
runStrategyElement conf sta env action = (newState, actions, retValue)
where
((retValue, actions), newState) = runReader @RConfig (runReaderT @REnv (runStateT @RState (runWriterT @RActions action) sta) env) conf
type EventCallback c s = forall m . MonadRobot m c s => Event -> m () type EventCallback c s = forall m . MonadRobot m c s => Event -> m ()
@ -77,40 +64,15 @@ data Event = NewBar Bar
| ActionCompleted Int Value | ActionCompleted Int Value
deriving (Show, Eq) deriving (Show, Eq)
data StrategyAction = ActionOrder Order
| ActionCancelOrder OrderId
| ActionLog T.Text
| ActionSetupTimer UTCTime
| ActionIO Int (IO Value)
data StrategyEnvironment = StrategyEnvironment { 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 _seBars :: !Bars, -- ^ List of tickers which is used by this strategy
seLastTimestamp :: !UTCTime _seLastTimestamp :: !UTCTime
} deriving (Eq) } deriving (Eq)
makeLenses ''StrategyEnvironment
instance Show StrategyAction where
show (ActionOrder order) = "ActionOrder " ++ show order
show (ActionCancelOrder oid) = "ActionCancelOrder " ++ show oid
show (ActionLog t) = "ActionLog " ++ show t
show (ActionIO x _) = "ActionIO " ++ show x
show (ActionSetupTimer t) = "ActionSetupTimer e" ++ show t
tellAction :: StrategyAction -> StrategyElement c s ()
tellAction a = tell @RActions [a]
instance MonadRobot (StrategyMonad c s) c s where
submitOrder order = tellAction $ ActionOrder order
cancelOrder oId = tellAction $ ActionCancelOrder oId
appendToLog = tellAction . ActionLog
setupTimer = tellAction . ActionSetupTimer
enqueueIOAction actionId action = tellAction $ ActionIO actionId action
getConfig = ask @RConfig
getState = get @RState
setState = put @RState
getEnvironment = ask @REnv
also :: EventCallback c s -> EventCallback c s -> EventCallback c s also :: EventCallback c s -> EventCallback c s -> EventCallback c s
also cb1 cb2 = (\event -> cb1 event >> cb2 event) also cb1 cb2 = (\event -> cb1 event >> cb2 event)

139
src/ATrade/RoboCom/Positions.hs

@ -63,6 +63,7 @@ module ATrade.RoboCom.Positions
exitAtLimit, exitAtLimit,
doNothing, doNothing,
setStopLoss, setStopLoss,
setLimitStopLoss,
setTakeProfit, setTakeProfit,
setStopLossAndTakeProfit setStopLossAndTakeProfit
) where ) where
@ -73,13 +74,14 @@ import ATrade.RoboCom.Monad
import ATrade.RoboCom.Types import ATrade.RoboCom.Types
import ATrade.Types import ATrade.Types
import Control.Lens
import Control.Monad import Control.Monad
import Ether
import Data.Aeson import Data.Aeson
import qualified Data.List as L import qualified Data.List as L
import qualified Data.Map as M 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 Data.Time.Clock import Data.Time.Clock
data PositionState = PositionWaitingOpenSubmission Order data PositionState = PositionWaitingOpenSubmission Order
@ -145,7 +147,7 @@ modifyPositions f = do
class ParamsHasMainTicker a where class ParamsHasMainTicker a where
mainTicker :: a -> TickerId mainTicker :: a -> TickerId
-- | Helper function. Finds first element in list which satisfies predicate 'p' and if found, applies 'm' to it, leaving other elements inact. -- | 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]
findAndModify p m (x:xs) = if p x findAndModify p m (x:xs) = if p x
then m x : xs then m x : xs
@ -186,7 +188,7 @@ dispatchPosition event pos = case posState pos of
PositionCancelled -> handlePositionCancelled pos PositionCancelled -> handlePositionCancelled pos
where where
handlePositionWaitingOpenSubmission pendingOrder = do handlePositionWaitingOpenSubmission pendingOrder = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if orderDeadline (posSubmissionDeadline pos) lastTs if orderDeadline (posSubmissionDeadline pos) lastTs
then return $ pos { posState = PositionCancelled } -- TODO call TimeoutHandler if present then return $ pos { posState = PositionCancelled } -- TODO call TimeoutHandler if present
else case event of else case event of
@ -199,52 +201,55 @@ dispatchPosition event pos = case posState pos of
_ -> return pos _ -> return pos
handlePositionWaitingOpen = do handlePositionWaitingOpen = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
case posCurrentOrder pos of case posCurrentOrder pos of
Just order -> if orderDeadline (posExecutionDeadline pos) lastTs Just order -> if orderDeadline (posExecutionDeadline pos) lastTs
then do -- TODO call TimeoutHandler then
appendToLog $ [st|"In PositionWaitingOpen: execution timeout: %?/%?"|] (posExecutionDeadline pos) lastTs if posBalance pos == 0
cancelOrder $ orderId order then do
return $ pos { posState = PositionWaitingPendingCancellation, posNextState = Just PositionCancelled } appendToLog $ [t|"In PositionWaitingOpen: execution timeout: %?/%?"|] (posExecutionDeadline pos) lastTs
cancelOrder $ orderId order
return $ pos { posState = PositionWaitingPendingCancellation, posNextState = Just PositionCancelled }
else do
appendToLog $ [t|Order executed (partially, %? / %?): %?|] (posBalance pos) (orderQuantity order) order
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 $ [st|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 $ [st|Order executed: %?|] order appendToLog $ [t|Order executed: %?|] order
return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posBalance = balanceForOrder order, posEntryTime = Just lastTs} return pos { posState = PositionOpen, posCurrentOrder = Nothing, posExecutionDeadline = Nothing, posBalance = balanceForOrder order, posEntryTime = Just lastTs}
Rejected -> do Rejected -> do
appendToLog $ [st|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 $ [st|In PositionWaitingOpen: order state update: %?|] newstate appendToLog $ [t|In PositionWaitingOpen: order state update: %?|] newstate
return pos return pos
else do else return pos -- Update for another position's order
appendToLog $ [st|Invalid order id: %?/%?|] oid (orderId order)
return pos
NewTrade trade -> do NewTrade trade -> do
appendToLog $ [st|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 $ [st|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 <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if if
| orderDeadline (posSubmissionDeadline pos) lastTs -> do | orderDeadline (posSubmissionDeadline pos) lastTs -> do
appendToLog $ [st|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 $ [st|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
@ -261,7 +266,7 @@ dispatchPosition event pos = case posState pos of
_ -> return pos _ -> return pos
handlePositionWaitingPendingCancellation = do handlePositionWaitingPendingCancellation = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if not $ orderDeadline (posSubmissionDeadline pos) lastTs if not $ orderDeadline (posSubmissionDeadline pos) lastTs
then case (event, posCurrentOrder pos, posNextState pos) of then case (event, posCurrentOrder pos, posNextState pos) of
(OrderUpdate _ newstate, Just _, Just (PositionWaitingCloseSubmission nextOrder)) -> (OrderUpdate _ newstate, Just _, Just (PositionWaitingCloseSubmission nextOrder)) ->
@ -280,7 +285,7 @@ dispatchPosition event pos = case posState pos of
return pos { posState = PositionCancelled } return pos { posState = PositionCancelled }
handlePositionWaitingCloseSubmission pendingOrder = do handlePositionWaitingCloseSubmission pendingOrder = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if orderDeadline (posSubmissionDeadline pos) lastTs if orderDeadline (posSubmissionDeadline pos) lastTs
then do then do
case posCurrentOrder pos of case posCurrentOrder pos of
@ -297,12 +302,13 @@ dispatchPosition event pos = case posState pos of
_ -> return pos _ -> return pos
handlePositionWaitingClose = do handlePositionWaitingClose = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
if orderDeadline (posExecutionDeadline pos) lastTs if orderDeadline (posExecutionDeadline pos) lastTs
then do then do
case posCurrentOrder pos of case posCurrentOrder pos of
Just order -> cancelOrder (orderId order) Just order -> cancelOrder (orderId order)
_ -> doNothing _ -> doNothing
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) ->
@ -312,6 +318,11 @@ dispatchPosition event pos = case posState pos of
posBalance = 0, posBalance = 0,
posSubmissionDeadline = Nothing } posSubmissionDeadline = Nothing }
else pos else pos
(NewTrade trade, Just order) ->
return $ if (tradeOrderId trade == orderId order)
then pos { posBalance = if tradeOperation trade == Buy then posBalance pos + tradeQuantity trade else posBalance pos - tradeQuantity trade }
else pos
_ -> return pos _ -> return pos
handlePositionClosed = return handlePositionClosed = return
@ -335,9 +346,9 @@ dispatchPosition event pos = case posState pos of
newPosition :: (StateHasPositions s, MonadRobot m c s) => Order -> T.Text -> TickerId -> Operation -> Int -> NominalDiffTime -> m Position newPosition :: (StateHasPositions s, MonadRobot m c s) => Order -> T.Text -> TickerId -> Operation -> Int -> NominalDiffTime -> m Position
newPosition order account tickerId operation quantity submissionDeadline = do newPosition order account tickerId operation quantity submissionDeadline = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
let position = Position { let position = Position {
posId = [st|%?/%?/%?/%?/%?|] account tickerId operation quantity lastTs, posId = TL.toStrict $ [t|%?/%?/%?/%?/%?|] account tickerId operation quantity lastTs,
posAccount = account, posAccount = account,
posTicker = tickerId, posTicker = tickerId,
posBalance = 0, posBalance = 0,
@ -354,12 +365,12 @@ newPosition order account tickerId operation quantity submissionDeadline = do
} }
modifyPositions (\p -> position : p) modifyPositions (\p -> position : p)
positions <- getPositions <$> getState positions <- getPositions <$> getState
appendToLog $ [st|All positions: %?|] positions appendToLog $ [t|All positions: %?|] positions
return position return position
reapDeadPositions :: (StateHasPositions s) => EventCallback c s reapDeadPositions :: (StateHasPositions s) => EventCallback c s
reapDeadPositions _ = do reapDeadPositions _ = do
ts <- seLastTimestamp <$> getEnvironment ts <- view seLastTimestamp <$> getEnvironment
when (floor (utctDayTime ts) `mod` 300 == 0) $ modifyPositions (L.filter (not . posIsDead)) when (floor (utctDayTime ts) `mod` 300 == 0) $ modifyPositions (L.filter (not . posIsDead))
defaultHandler :: (StateHasPositions s) => EventCallback c s defaultHandler :: (StateHasPositions s) => EventCallback c s
@ -377,15 +388,15 @@ modifyPosition f oldpos = do
getCurrentTicker :: (ParamsHasMainTicker c, MonadRobot m c s) => m [Bar] getCurrentTicker :: (ParamsHasMainTicker c, MonadRobot m c s) => m [Bar]
getCurrentTicker = do getCurrentTicker = do
bars <- seBars <$> getEnvironment mainTicker' <- mainTicker <$> getConfig
maybeBars <- flip M.lookup bars . mainTicker <$> getConfig 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 :: (ParamsHasMainTicker c, MonadRobot m c s) => m (Maybe BarSeries) getCurrentTickerSeries :: (ParamsHasMainTicker c, MonadRobot m c s) => m (Maybe BarSeries)
getCurrentTickerSeries = do getCurrentTickerSeries = do
bars <- seBars <$> getEnvironment bars <- view seBars <$> getEnvironment
flip M.lookup bars . mainTicker <$> getConfig 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)
@ -447,9 +458,9 @@ onActionCompletedEvent event f = case event of
_ -> doNothing _ -> doNothing
enterAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Operation -> m Position enterAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Operation -> m Position
enterAtMarket signalName operation = do enterAtMarket operationSignalName operation = do
env <- getEnvironment env <- getEnvironment
enterAtMarketWithParams (seAccount env) (seVolume env) (SignalId (seInstanceId env) signalName "") operation enterAtMarketWithParams (env ^. seAccount) (env ^. seVolume) (SignalId (env ^. seInstanceId) operationSignalName "") operation
enterAtMarketWithParams :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Int -> SignalId -> Operation -> m Position enterAtMarketWithParams :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> Int -> SignalId -> Operation -> m Position
enterAtMarketWithParams account quantity signalId operation = do enterAtMarketWithParams account quantity signalId operation = do
@ -467,15 +478,15 @@ enterAtMarketWithParams account quantity signalId operation = do
} }
enterAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Price -> Operation -> m Position enterAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Price -> Operation -> m Position
enterAtLimit timeToCancel signalName price operation = do enterAtLimit timeToCancel operationSignalName price operation = do
env <- getEnvironment env <- getEnvironment
enterAtLimitWithParams timeToCancel (seAccount env) (seVolume env) (SignalId (seInstanceId env) signalName "") 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 :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> T.Text -> Price -> Int -> Operation -> m Position
enterAtLimitWithVolume timeToCancel signalName price vol operation = do enterAtLimitWithVolume timeToCancel operationSignalName price vol operation = do
acc <- seAccount <$> getEnvironment acc <- view seAccount <$> getEnvironment
inst <- seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
enterAtLimitWithParams timeToCancel acc vol (SignalId inst signalName "") price operation 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 :: (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 enterAtLimitWithParams timeToCancel account quantity signalId price operation = do
@ -483,23 +494,23 @@ enterAtLimitWithParams timeToCancel account quantity signalId price operation =
enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId price operation 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 :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Price -> Int -> Operation -> m Position
enterAtLimitForTickerWithVolume tickerId timeToCancel signalName price vol operation = do enterAtLimitForTickerWithVolume tickerId timeToCancel operationSignalName price vol operation = do
acc <- seAccount <$> getEnvironment acc <- view seAccount <$> getEnvironment
inst <- seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst signalName "") price operation enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst operationSignalName "") price operation
enterAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Price -> Operation -> m Position enterAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Price -> Operation -> m Position
enterAtLimitForTicker tickerId timeToCancel signalName price operation = do enterAtLimitForTicker tickerId timeToCancel operationSignalName price operation = do
acc <- seAccount <$> getEnvironment acc <- view seAccount <$> getEnvironment
inst <- seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
vol <- seVolume <$> getEnvironment vol <- view seVolume <$> getEnvironment
enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst signalName "") price operation enterAtLimitForTickerWithParams tickerId timeToCancel acc vol (SignalId inst operationSignalName "") price operation
enterAtLimitForTickerWithParams :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> T.Text -> Int -> SignalId -> Price -> Operation -> m Position 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 <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
submitOrder order submitOrder order
appendToLog $ [st|enterAtLimit: %?, deadline: %?|] tickerId (timeToCancel `addUTCTime` lastTs) appendToLog $ [t|enterAtLimit: %?, deadline: %?|] tickerId (timeToCancel `addUTCTime` lastTs)
newPosition order 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
@ -513,27 +524,27 @@ enterAtLimitForTickerWithParams tickerId timeToCancel account quantity signalId
} }
enterLongAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position enterLongAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position
enterLongAtMarket signalName = enterAtMarket signalName Buy enterLongAtMarket operationSignalName = enterAtMarket operationSignalName Buy
enterShortAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position enterShortAtMarket :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => T.Text -> m Position
enterShortAtMarket signalName = enterAtMarket signalName Sell enterShortAtMarket operationSignalName = enterAtMarket operationSignalName Sell
enterLongAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position enterLongAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position
enterLongAtLimit timeToCancel price signalName = enterAtLimit timeToCancel signalName price Buy enterLongAtLimit timeToCancel price operationSignalName = enterAtLimit timeToCancel operationSignalName price Buy
enterLongAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position enterLongAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position
enterLongAtLimitForTicker tickerId timeToCancel price signalName = enterAtLimitForTicker tickerId timeToCancel signalName price Buy enterLongAtLimitForTicker tickerId timeToCancel price operationSignalName = enterAtLimitForTicker tickerId timeToCancel operationSignalName price Buy
enterShortAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position enterShortAtLimit :: (StateHasPositions s, ParamsHasMainTicker c, MonadRobot m c s) => NominalDiffTime -> Price -> T.Text -> m Position
enterShortAtLimit timeToCancel price signalName = enterAtLimit timeToCancel signalName price Sell enterShortAtLimit timeToCancel price operationSignalName = enterAtLimit timeToCancel operationSignalName price Sell
enterShortAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position enterShortAtLimitForTicker :: (StateHasPositions s, MonadRobot m c s) => TickerId -> NominalDiffTime -> Price -> T.Text -> m Position
enterShortAtLimitForTicker tickerId timeToCancel price signalName = enterAtLimitForTicker tickerId timeToCancel signalName 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 signalName = do exitAtMarket position operationSignalName = do
inst <- seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
case posCurrentOrder position of case posCurrentOrder position of
Just order -> do Just order -> do
cancelOrder (orderId order) cancelOrder (orderId order)
@ -558,18 +569,18 @@ exitAtMarket position signalName = do
orderQuantity = (abs . posBalance) position, orderQuantity = (abs . posBalance) position,
orderPrice = Market, orderPrice = Market,
orderOperation = if posBalance position > 0 then Sell else Buy, orderOperation = if posBalance position > 0 then Sell else Buy,
orderSignalId = (SignalId inst signalName "") orderSignalId = (SignalId inst operationSignalName "")
} }
exitAtLimit :: (StateHasPositions s, MonadRobot m c s) => NominalDiffTime -> Price -> Position -> T.Text -> m Position exitAtLimit :: (StateHasPositions s, MonadRobot m c s) => NominalDiffTime -> Price -> Position -> T.Text -> m Position
exitAtLimit timeToCancel price position signalName = do exitAtLimit timeToCancel price position operationSignalName = do
lastTs <- seLastTimestamp <$> getEnvironment lastTs <- view seLastTimestamp <$> getEnvironment
inst <- seInstanceId <$> getEnvironment inst <- view seInstanceId <$> getEnvironment
case posCurrentOrder position of case posCurrentOrder position of
Just order -> cancelOrder (orderId order) Just order -> cancelOrder (orderId order)
Nothing -> doNothing Nothing -> doNothing
submitOrder (closeOrder inst) submitOrder (closeOrder inst)
appendToLog $ [st|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), posState = PositionWaitingCloseSubmission (closeOrder inst),
@ -583,7 +594,7 @@ exitAtLimit timeToCancel price position signalName = do
orderQuantity = (abs . posBalance) position, orderQuantity = (abs . posBalance) position,
orderPrice = Limit price, orderPrice = Limit price,
orderOperation = if posBalance position > 0 then Sell else Buy, orderOperation = if posBalance position > 0 then Sell else Buy,
orderSignalId = SignalId inst signalName "" orderSignalId = SignalId inst operationSignalName ""
} }
doNothing :: (MonadRobot m c s) => m () doNothing :: (MonadRobot m c s) => m ()

15
src/ATrade/RoboCom/Types.hs

@ -10,7 +10,8 @@ module ATrade.RoboCom.Types (
Timeframe(..), Timeframe(..),
tfSeconds, tfSeconds,
Ticker(..), Ticker(..),
Bars Bars,
InstrumentParameters(..)
) where ) where
import ATrade.Types import ATrade.Types
@ -19,9 +20,6 @@ import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM 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 qualified Data.Text.Lazy as TL
import Data.Time.Clock
import Text.Read hiding (String)
newtype Timeframe = newtype Timeframe =
Timeframe Integer deriving (Show, Eq) Timeframe Integer deriving (Show, Eq)
@ -29,11 +27,18 @@ newtype Timeframe =
tfSeconds :: (Num a) => Timeframe -> a tfSeconds :: (Num a) => Timeframe -> a
tfSeconds (Timeframe s) = fromInteger s tfSeconds (Timeframe s) = fromInteger s
data InstrumentParameters =
InstrumentParameters {
ipLotSize :: Int,
ipTickSize :: Price
} deriving (Show, Eq)
data BarSeries = data BarSeries =
BarSeries { BarSeries {
bsTickerId :: TickerId, bsTickerId :: TickerId,
bsTimeframe :: Timeframe, bsTimeframe :: Timeframe,
bsBars :: [Bar] bsBars :: [Bar],
bsParams :: InstrumentParameters
} deriving (Show, Eq) } deriving (Show, Eq)
-- | Ticker description record -- | Ticker description record

9
src/ATrade/RoboCom/Utils.hs

@ -9,18 +9,18 @@ module ATrade.RoboCom.Utils (
barNumber, barNumber,
getHMS, getHMS,
getHMS', getHMS',
fromHMS,
fromHMS', fromHMS',
parseTime parseTime
) where ) where
import ATrade.Types import ATrade.Types
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Lazy.Builder
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.Clock import Data.Time.Clock
import Text.Read hiding (String) import Text.Read hiding (String)
rescaleToDaily :: [Bar] -> [Bar] rescaleToDaily :: [Bar] -> [Bar]
rescaleToDaily (firstBar:restBars) = rescaleToDaily' restBars firstBar rescaleToDaily (firstBar:restBars) = rescaleToDaily' restBars firstBar
@ -67,6 +67,9 @@ fromHMS' hms = fromIntegral $ h * 3600 + m * 60 + s
m = (hms `mod` 10000) `div` 100 m = (hms `mod` 10000) `div` 100
s = (hms `mod` 100) s = (hms `mod` 100)
fromHMS :: Int -> Int -> Int -> DiffTime
fromHMS h m s = fromIntegral $ h * 3600 + m * 60 + s
parseTime :: T.Text -> Maybe DiffTime parseTime :: T.Text -> Maybe DiffTime
parseTime x = case readMaybe (T.unpack x) of parseTime x = case readMaybe (T.unpack x) of
Just t -> let h = t `div` 10000 Just t -> let h = t `div` 10000

6
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-12.9 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.
@ -46,10 +46,8 @@ extra-deps:
- list-extras-0.4.1.4 - list-extras-0.4.1.4
- snowball-1.0.0.1 - snowball-1.0.0.1
- binary-ieee754-0.1.0.0 - binary-ieee754-0.1.0.0
- th-printf-0.5.1 - th-printf-0.7
- normaldistribution-1.1.0.3 - normaldistribution-1.1.0.3
- text-format-0.3.2
- ether-0.5.1.0
# Override default flag values for local packages and extra-deps # Override default flag values for local packages and extra-deps
# flags: {} # flags: {}

66
test/Test/BarAggregator.hs

@ -34,6 +34,7 @@ unitTests = testGroup "BarAggregator" [
, testTwoBarsInSameBar , testTwoBarsInSameBar
, testTwoBarsInSameBarLastBar , testTwoBarsInSameBarLastBar
, testNextBarAfterBarClose , testNextBarAfterBarClose
, testUpdateTime
] ]
properties = testGroup "BarAggregator" [ properties = testGroup "BarAggregator" [
@ -194,6 +195,37 @@ testNextBarAfterBarClose = testCase "Three bars (smaller timeframe) - next bar a
barClose = fromDouble c, barClose = fromDouble c,
barVolume = v } 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 }
prop_allTicksInOneBar :: TestTree prop_allTicksInOneBar :: TestTree
prop_allTicksInOneBar = testProperty "All ticks in one bar" $ property $ do prop_allTicksInOneBar = testProperty "All ticks in one bar" $ property $ do
tf <- forAll $ Gen.integral (Range.constant 1 86400) tf <- forAll $ Gen.integral (Range.constant 1 86400)
@ -219,37 +251,3 @@ prop_allTicksInOneBar = testProperty "All ticks in one bar" $ property $ do
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
prop_ticksInTwoBars :: TestTree
prop_ticksInTwoBars = testProperty "Ticks in one bar, then in next bar" $ property $ do
tf <- forAll $ Gen.integral (Range.constant 1 86400)
ticks1 <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" (baseTime 0) tf)
ticks2 <- forAll $ Gen.list (Range.linear 1 100) (genTick "TEST_TICKER" (baseTime $ secondsToDiffTime tf) tf)
let ticks1' = sortOn timestamp ticks1
let ticks2' = sortOn timestamp ticks2
let (_, agg) = handleTicks ticks1' (mkAggregator "TEST_TICKER" tf)
let ([newbar], agg') = handleTicks ticks2' agg
barSecurity newbar === "TEST_TICKER"
(barHigh newbar) === (maximum $ value <$> ticks1)
(barLow newbar) === (minimum $ value <$> ticks1)
(barOpen newbar) === (value . head $ ticks1')
(barClose newbar) === (value . last $ ticks1')
(barVolume newbar) === (sum $ volume <$> ticks1)
(barHigh <$> currentBar "TEST_TICKER" agg') === Just (maximum $ value <$> ticks2)
(barLow <$> currentBar "TEST_TICKER" agg') === Just (minimum $ value <$> ticks2)
(barOpen <$> currentBar "TEST_TICKER" agg') === (value <$> headMay ticks2')
(barClose <$> currentBar "TEST_TICKER" agg') === (value <$> lastMay ticks2')
(barVolume <$> currentBar "TEST_TICKER" agg') === Just (sum $ volume <$> ticks2)
where
genTick :: T.Text -> UTCTime -> Integer -> Gen Tick
genTick tickerId base tf = do
difftime <- fromRational . toRational . picosecondsToDiffTime <$> Gen.integral (Range.linear 0 (truncate 1e12 * tf))
val <- fromDouble <$> Gen.double (Range.exponentialFloat 0.00001 100)
vol <- Gen.integral (Range.exponential 1 100)
return $ Tick tickerId LastTradePrice (difftime `addUTCTime` base) 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))
baseTime offset = UTCTime (fromGregorian 1970 1 1) offset

Loading…
Cancel
Save