This commit is contained in:
Nicole Dresselhaus 2022-03-14 20:12:24 +01:00
commit befb1ab1eb
Signed by: Drezil
GPG Key ID: AC88BB432537313A
23 changed files with 1973 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*~
*.swp
tarballs/
.stack-work/
imgui.ini
settings.json
*.lock
tags

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "deps/dear-implot.hs"]
path = deps/dear-implot.hs
url = https://github.com/Drezil/dear-implot.hs
[submodule "deps/dear-imgui.hs"]
path = deps/dear-imgui.hs
url = https://github.com/haskell-game/dear-imgui.hs

0
.hlint.yaml Normal file
View File

3
ChangeLog.md Normal file
View File

@ -0,0 +1,3 @@
# Changelog for ibhelper
## Unreleased changes

30
LICENSE Normal file
View File

@ -0,0 +1,30 @@
Copyright Stefan Dresselhaus (c) 2019
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Author name here nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE 2022 Stefan Dresselhaus HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 2022 Stefan Dresselhaus
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# ibhelper
## Execute
* Run `stack exec -- ibhelper-exe` to see "We're inside the application!"
* With `stack exec -- ibhelper-exe --verbose` you will see the same message, with more logging.
## Run tests
`stack test`

2
Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

108
app/Main.hs Normal file
View File

@ -0,0 +1,108 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Import
import Data.Aeson (eitherDecodeFileStrict')
import Control.Monad.Managed
import Control.Concurrent
import DearImGui
import DearImGui.OpenGL3
import DearImGui.GLFW
import DearImGui.GLFW.OpenGL
import Run
import RIO.Process
import System.Directory
import Options.Applicative.Simple
import qualified Paths_ibhelper
import qualified Graphics.UI.GLFW as GLFW
import qualified Data.Text as T
import Prelude (putStrLn)
import AppFiller
main :: IO ()
main = do
(options, ()) <- simpleOptions
$(simpleVersion Paths_ibhelper.version)
"Header for command line arguments"
"Program description, also for command line arguments"
(Options
<$> switch ( long "verbose"
<> short 'v'
<> help "Verbose output?"
)
)
empty
settingsFileExists <- doesFileExist "settings.json"
settings <- if settingsFileExists
then do
s <- fmap unDefaultJSON <$> eitherDecodeFileStrict' "settings.json"
pPrint s
case s of
Left e -> putStrLn ("Error loading settings: \n"<>e) >> return def
Right s' -> return s'
else return def
lo <- logOptionsHandle stderr (optionsVerbose options)
<&> setLogMinLevel (settings ^. logLevel)
<&> setLogTerminal True
pc <- mkDefaultProcessContext
withLogFunc lo $ \lf -> do
-- let bare_log = unLogFunc $ view logFuncL lf
-- logErr = liftIO . bare_log callStack "" LevelError
initialized <- GLFW.init
unless initialized $ error "GLFW init failed"
liftIO $ runManaged $ do
mwin <- managed $ bracket
(GLFW.createWindow (settings ^. windowParams . windowWidth) (settings ^. windowParams . windowHeight) "IB-Helper" Nothing Nothing)
(maybe (return ()) GLFW.destroyWindow)
case mwin of
Just win -> do
liftIO $ do
GLFW.makeContextCurrent (Just win)
GLFW.swapInterval 1
-- Create an ImGui context
_ <- managed $ bracket createContext destroyContext
-- Create an ImPlot context
-- _ <- managed $ bracket createPlotContext destroyPlotContext
-- Initialize ImGui's GLFW backend
_ <- managed_ $ bracket_ (glfwInitForOpenGL win True) glfwShutdown
-- Initialize ImGui's OpenGL backend
_ <- managed_ $ bracket_ openGL3Init openGL3Shutdown
twsConnectionRefsHost <- liftIO $ atomically $ newTVar $ settings ^. twsConnection . host . to T.unpack
twsConnectionRefsPort <- liftIO $ atomically $ newTVar $ settings ^. twsConnection . port . to T.unpack
twsConnectionStatus <- liftIO $ atomically $ newTVar $ TWSDisconnected
twsConnectionSend <- liftIO $ atomically $ newTQueue
twsConnectionRecieve <- liftIO $ atomically $ newTQueue
let twsConnectionRefs = TWSConnectionRefs{..}
liftIO $ atomically $ writeTQueue twsConnectionSend $ Msg_IB_OUT $ IB_RequestMarketDataType DelayedFrozen
currentAccount <- liftIO $ newTVarIO $ Nothing
tickerIdToSymbol <- liftIO $ newTVarIO $ mempty
appCharts <- liftIO $ newTVarIO $ mempty
appData <- liftIO $ DataRefs
<$> newTVarIO mempty
<*> newTVarIO Nothing
<*> newTVarIO mempty
<*> newTVarIO mempty
let app = App
{ appSettings = settings
, appLogFunc = lf
, appProcessContext = pc
, appOptions = options
, appWindow = win
, appRefs = AppRefs{..}
, appData = appData
}
void $ liftIO $ forkIO $ appFiller app
liftIO $ runRIO app run
Nothing -> do
error "GLFW createWindow failed"
GLFW.terminate

1
deps/dear-imgui.hs vendored Submodule

@ -0,0 +1 @@
Subproject commit e5969f6b358dab01d9e2bea8d3538fdfbee0c2f1

1
deps/dear-implot.hs vendored Submodule

@ -0,0 +1 @@
Subproject commit 78f7df091ceb88f57eaacdb0a1e2d43cbce04566

248
ibhelper.cabal Normal file
View File

@ -0,0 +1,248 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
name: ibhelper
version: 0.1.0.0
description: Please see the README.md
homepage: https://github.com/Drezil/ibhelper#readme
bug-reports: https://github.com/Drezil/ibhelper/issues
author: Stefan Dresselhaus
maintainer: sdressel@pwning.de
copyright: 2022 Stefan Dresselhaus
license: BSD3
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
ChangeLog.md
source-repository head
type: git
location: https://github.com/Drezil/ibhelper
library
exposed-modules:
AppFiller
Chart
IBClient.Connection
IBClient.Types
Import
Run
Types
Util
other-modules:
Paths_ibhelper
hs-source-dirs:
src
default-extensions:
BangPatterns
BinaryLiterals
ConstraintKinds
DataKinds
DefaultSignatures
DeriveDataTypeable
DeriveFoldable
DeriveFunctor
DeriveGeneric
DeriveTraversable
DoAndIfThenElse
EmptyDataDecls
ExistentialQuantification
FlexibleContexts
FlexibleInstances
FunctionalDependencies
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NoImplicitPrelude
OverloadedStrings
PartialTypeSignatures
PatternGuards
PolyKinds
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TupleSections
TypeFamilies
TypeSynonymInstances
ViewPatterns
DuplicateRecordFields
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints
build-depends:
GLFW-b
, StateVar
, aeson
, base >=4.11 && <10
, binary
, bytestring
, data-default
, dear-imgui
, directory
, fingertree
, gl
, managed
, microlens-th
, network
, pretty-show
, rio >=0.1.12.0
, stm
, text
, time
, type-iso
, unordered-containers
default-language: Haskell2010
executable ibhelper-exe
main-is: Main.hs
other-modules:
Paths_ibhelper
hs-source-dirs:
app
default-extensions:
BangPatterns
BinaryLiterals
ConstraintKinds
DataKinds
DefaultSignatures
DeriveDataTypeable
DeriveFoldable
DeriveFunctor
DeriveGeneric
DeriveTraversable
DoAndIfThenElse
EmptyDataDecls
ExistentialQuantification
FlexibleContexts
FlexibleInstances
FunctionalDependencies
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NoImplicitPrelude
OverloadedStrings
PartialTypeSignatures
PatternGuards
PolyKinds
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TupleSections
TypeFamilies
TypeSynonymInstances
ViewPatterns
DuplicateRecordFields
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
build-depends:
GLFW-b
, StateVar
, aeson
, base >=4.11 && <10
, binary
, bytestring
, data-default
, dear-imgui
, directory
, fingertree
, gl
, ibhelper
, managed
, microlens-th
, network
, optparse-simple
, pretty-show
, rio >=0.1.12.0
, stm
, text
, time
, type-iso
, unordered-containers
default-language: Haskell2010
test-suite ibhelper-test
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
UtilSpec
Paths_ibhelper
hs-source-dirs:
test
default-extensions:
BangPatterns
BinaryLiterals
ConstraintKinds
DataKinds
DefaultSignatures
DeriveDataTypeable
DeriveFoldable
DeriveFunctor
DeriveGeneric
DeriveTraversable
DoAndIfThenElse
EmptyDataDecls
ExistentialQuantification
FlexibleContexts
FlexibleInstances
FunctionalDependencies
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NoImplicitPrelude
OverloadedStrings
PartialTypeSignatures
PatternGuards
PolyKinds
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TupleSections
TypeFamilies
TypeSynonymInstances
ViewPatterns
DuplicateRecordFields
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
build-depends:
GLFW-b
, StateVar
, aeson
, base >=4.11 && <10
, binary
, bytestring
, data-default
, dear-imgui
, directory
, fingertree
, gl
, hspec
, ibhelper
, managed
, microlens-th
, network
, pretty-show
, rio >=0.1.12.0
, stm
, text
, time
, type-iso
, unordered-containers
default-language: Haskell2010

121
package.yaml Normal file
View File

@ -0,0 +1,121 @@
name: ibhelper
version: 0.1.0.0
github: Drezil/ibhelper
license: BSD3
author: Stefan Dresselhaus
maintainer: sdressel@pwning.de
copyright: 2022 Stefan Dresselhaus
extra-source-files:
- README.md
- ChangeLog.md
# Metadata used when publishing your package
# synopsis: Short description of your package
# category: Web
# To avoid duplicated efforts in documentation and dealing with the
# complications of embedding Haddock markup inside cabal files, it is
# common to point users to the README.md file.
description: Please see the README.md
default-extensions:
- BangPatterns
- BinaryLiterals
- ConstraintKinds
- DataKinds
- DefaultSignatures
- DeriveDataTypeable
- DeriveFoldable
- DeriveFunctor
- DeriveGeneric
- DeriveTraversable
- DoAndIfThenElse
- EmptyDataDecls
- ExistentialQuantification
- FlexibleContexts
- FlexibleInstances
- FunctionalDependencies
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NamedFieldPuns
- NoImplicitPrelude
- OverloadedStrings
- PartialTypeSignatures
- PatternGuards
- PolyKinds
- RankNTypes
- RecordWildCards
- ScopedTypeVariables
- StandaloneDeriving
- TupleSections
- TypeFamilies
- TypeSynonymInstances
- ViewPatterns
- DuplicateRecordFields
dependencies:
- base >= 4.11 && < 10
- rio >= 0.1.12.0
- dear-imgui
- GLFW-b
- managed
- gl
- aeson
- data-default
- directory
- microlens-th
- network
- bytestring
- stm
- text
- pretty-show
- StateVar
- type-iso
- binary
- time
- unordered-containers
- fingertree
ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wpartial-fields
- -Wredundant-constraints
library:
source-dirs: src
executables:
ibhelper-exe:
main: Main.hs
source-dirs: app
dependencies:
- ibhelper
- optparse-simple
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
tests:
ibhelper-test:
main: Spec.hs
source-dirs: test
dependencies:
- ibhelper
- hspec
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N

79
src/AppFiller.hs Normal file
View File

@ -0,0 +1,79 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE TypeApplications #-}
module AppFiller where
import Import
import Types
import qualified Data.Text as T
import Data.Time
import Data.FingerTree
import Data.HashMap.Strict ((!?))
import qualified Data.HashMap.Strict as HM
import qualified Debug.Trace as D
appFiller :: App -> IO ()
appFiller app = runRIO app $ withRunInIO $ \run -> do
let queue = twsConnectionRecieve . twsConnectionRefs . appRefs $ app
debugMsg x = run $ logDebug (display $ T.pack $ "FILLER : " <> T.unpack x)
infoMsg x = run $ logDebug (display $ T.pack $ "FILLER : " <> T.unpack x)
forever $ do
input <- atomically $ readTQueue queue
let currentAppData = appData app
case input of
(Msg_IB_IN IB_PositionData) -> return ()
(Msg_IB_IN (IB_ManagedAccts as)) -> do
cur <- readTVarIO $ Types.accounts currentAppData
actions <- forM as $ \a -> case cur !? a of
Just _ -> return $ id
Nothing -> do
debugMsg $ "added Account "<> a
return $ HM.insertWith const a (mkIBAccount a)
atomically $ modifyTVar (Types.accounts currentAppData) $ foldl' (.) id actions
(Msg_IB_IN (IB_NextValidID i)) -> atomically $ modifyTVar' (nextValidID currentAppData) (const (Just i))
(Msg_IB_IN (IB_ErrorMsg i c m)) -> debugMsg "IB_ErrorMsg not implemented"
(Msg_IB_IN (IB_AccountValue k v c n)) -> do
let action = HM.update (\ai -> Just $ ai & accountInfo . accountProperties %~ HM.alter (\old -> Just $ (v,c):filter ((/=c) . snd) (fromMaybe [] old)) k) n
atomically $ modifyTVar' (Types.accounts currentAppData) action
(Msg_IB_IN (IB_AccountUpdateTime t)) -> debugMsg "IB_AccountUpdateTime not implemented"
-- (Msg_IB_IN (IB_AccountUpdateTime t)) -> do
-- let action = HM.update (\ai -> Just $ ai & accountInfo . accountLastUpdate %~ const t) n
-- atomically $ modifyTVar' (Types.accounts currentAppData) action
(Msg_IB_IN (IB_PortfolioValue c p mp mv ac u r n)) -> do
let cid = conId :: IBContract -> Int
updateAction (a@IBPortfolioValue{..}:as)
| cid _contract == cid c = IBPortfolioValue c p mp mv ac u r:as
| otherwise = a:updateAction as
updateAction [] = [IBPortfolioValue c p mp mv ac u r]
action = HM.update (\ai -> Just $ ai & accountPortfolio %~ updateAction) n
atomically $ modifyTVar' (Types.accounts currentAppData) action
(Msg_IB_IN (IB_SymbolSamples r s)) -> do
atomically $ do
modifyTVar' (nextValidID currentAppData) (const $ Just r)
modifyTVar' (symbolLookupResults currentAppData) (const $ (\IB_SymbolSample{..} -> IBSymbolSample symId symbol secType primaryExchange currency derivatives) <$> s)
(Msg_IB_IN t@IB_TickPrice{}) -> run $ handleTickPrice t
_ -> --D.trace ("not implemented in AppFiller:" <> show input) $
infoMsg $ "not implemented in AppFiller:" <> T.pack (show input)
handleTickPrice :: IB_IN -> RIO App ()
handleTickPrice IB_TickPrice{..} = do
charts <- appCharts . appRefs <$> ask
tid2symbol <- tickerIdToSymbol . appRefs <$> ask
msymbol <- (HM.!? tickerId) <$> liftIO (readTVarIO tid2symbol)
case msymbol of
Nothing -> return () --ignore
Just s -> do
chartVar <- (HM.!s) <$> liftIO (readTVarIO charts)
case tickType of
IBTickType_Last_Price -> do
t <- utctDayTime <$> liftIO getCurrentTime
let cp = ChartPoint (TimePoint $ fromInteger. (`div` 1000000000000) . diffTimeToPicoseconds $ t) price []
liftIO $ atomically $ modifyTVar chartVar (\c@Chart{..} -> c { chartData = chartData |> cp, chartDirty = True})
_ -> return ()
handleTickPrice _ = error "impossible"

89
src/Chart.hs Normal file
View File

@ -0,0 +1,89 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DerivingVia #-}
module Chart (newChart, FillerException(..)) where
import Import
import Data.Time
import RIO.List
import RIO.List.Partial
import Data.FingerTree (FingerTree)
import Control.Concurrent (forkIO)
import qualified RIO.ByteString as BS
-- import Control.Exception
import qualified Data.HashMap.Strict as HM
import qualified Data.FingerTree as FT
import qualified Debug.Trace as D
data FillerException = QuitFiller
deriving Show
instance Exception FillerException
deriving via Integer instance Hashable Day
newChart :: IBContract -> RIO App ()
newChart contract = do
app <- ask
let sym = (symbol :: IBContract -> Text) contract
hmVar = appCharts . appRefs $ app
hm <- liftIO . readTVarIO $ hmVar
unless (sym `HM.member` hm) $ do
c <- liftIO $ newTVarIO $ Chart FT.empty mempty undefined defChartSettings [] Nothing False
tid <- liftIO $ forkIO $ fillChart app contract c
liftIO $ atomically $ do
modifyTVar' c (\Chart{..} -> let fillerThread = tid in Chart{..})
modifyTVar' hmVar (HM.insert sym c)
fillChart :: App -> IBContract -> TVar Chart -> IO ()
fillChart app contract cVar = runRIO app $ do
let sym = (symbol :: IBContract -> Text) contract
(tickerMapVar :: TVar (HashMap Int Text)) <- tickerIdToSymbol. appRefs <$> ask
alreadyAdded <- (sym `elem`) . HM.elems <$> liftIO (readTVarIO tickerMapVar)
unless alreadyAdded $ do
tickerId <- (+1) . foldl' max 1000 . HM.keys <$> liftIO (readTVarIO tickerMapVar)
let cancelSubscription = liftIO $ atomically $ do
modifyTVar tickerMapVar (HM.delete tickerId)
-- TODO: send cancel-request
let sendQ = twsConnectionSend $ twsConnectionRefs $ appRefs $ app
liftIO $ atomically $ do
modifyTVar tickerMapVar (HM.insert tickerId sym)
writeTQueue sendQ $ Msg_IB_OUT $ IB_RequestMktData tickerId contract "233" False False
handle (\QuitFiller -> cancelSubscription >> exitSuccess) $
forever $ do
-- chart dirty? set clean & begin work
Chart{..} <- liftIO (readTVarIO cVar)
when chartDirty $ do
liftIO $ atomically $ modifyTVar cVar (\c -> c { chartDirty = False })
let (TimePoint cacheUpdateStart) = fromMaybe (TimePoint 0) Nothing -- TODO: lastCacheUpdate
cacheUpdateEnd = 86400
chunkChart :: Int -> Int -> Int -> FingerTree TimePoint ChartPoint -> [(TimePoint,[ChartPoint])]
chunkChart from until range tree = go from range interval
where
lastItem = case FT.viewr interval of
FT.EmptyR -> until
(_ FT.:> ChartPoint{..}) -> (\(TimePoint x) -> x) timeOfDay
interval = FT.takeUntil (\(TimePoint x) -> x > until)
. FT.dropUntil (\(TimePoint x) -> x > from)
$ tree
go f i t
| f+i >= lastItem = [(TimePoint (f+i), toList t)]
| otherwise = let (a, b) = FT.split (\(TimePoint x) -> x > f+i) t
in (TimePoint (f+i),toList a) : go (f+i) i b
chunkedChart = chunkChart cacheUpdateStart cacheUpdateEnd (chartResolution chartSettings) chartData
cachePoints = takeWhile (\ChartPoint{..} -> (\(TimePoint x) -> x < cacheUpdateStart) timeOfDay) chartCache <> map toCachePoint chunkedChart
toCachePoint :: (TimePoint,[ChartPoint]) -> ChartPoint
toCachePoint (t,[]) = ChartPoint t (-1) []
toCachePoint (t,as) = ChartPoint t c [OLHC o l h c]
where
as' = pointValue <$> as
o = head as'
c = last as'
l = minimum as'
h = maximum as'
let lUpdate = fmap fst . lastMaybe $ chunkedChart
liftIO $ atomically $ modifyTVar cVar (\c -> c { chartCache = cachePoints, lastCacheUpdate = lUpdate })
return ()
threadDelay 1000000 -- sleep 5 seconds

View File

@ -0,0 +1,95 @@
{-# LANGUAGE TypeApplications #-}
module IBClient.Connection where
import Import
import Data.Binary
import Network.Socket
import Network.Socket.ByteString
import qualified Control.Exception as E
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Text as T
forkClient :: App -> IO ()
forkClient app = runRIO app $ withRunInIO $ \run -> withSocketsDo $ do
let refs = twsConnectionRefs $ appRefs app
toSend = twsConnectionSend refs
toRecieve = twsConnectionRecieve refs
cStatus = twsConnectionStatus refs
debugSend x = run $ logDebug (display $ T.pack $ "SENT : " <> show x)
debugRecv x = run $ logDebug (display $ T.pack $ "RECIEVED: " <> show x)
connHost <- readTVarIO $ twsConnectionRefsHost refs
connPort <- readTVarIO $ twsConnectionRefsPort refs
atomically $ modifyTVar' cStatus (const TWSConnecting)
-- TODO: throws IO-Exeption instead of returning empty list -> handle!
addr:_ <- getAddrInfo (Just $ defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV], addrProtocol = 0, addrSocketType = Stream}) (Just connHost) (Just connPort)
run $ logDebug $ displayShow addr
E.bracketOnError (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)) close $ \sock -> do
connect sock $ addrAddress addr
let connStr = BS8.pack "API\0\0\0\0\tv100..157"
sendAll sock connStr
debugSend connStr
answer <- recvAll sock
run $ logDebug (displayShow (decode . LBS.fromStrict <$> answer :: Maybe IBGenericMessage))
-- if we have the answer we are connected
let idStr = LBS.toStrict $ encode $ IB_StartAPI "2" "69" -- version 2, client-id: 69
sendAll sock idStr
debugSend idStr
atomically $ modifyTVar' cStatus (const TWSConnected)
run $ logInfo $ display ("Connected to TWS" :: Text)
let go True = do
-- abort connection, close everything
return ()
go False = do
-- race: wait for MSG in Queue or for answer on socket
input <- race (atomically $ readTQueue toSend)
(recvAll sock)
case input of
-- we want to disconnect
Left IBDisconnect -> go True
Left (Msg_IB_OUT x) -> do
let msg = LBS.toStrict $ encode x
debugSend msg
sendAll sock msg
-- we lost connection
Right Nothing -> do
atomically $ do
writeTQueue toRecieve IBServerGone
modifyTVar' cStatus (const TWSDisconnected)
run $ logWarn $ display $ T.pack "Lost connection to TWS, reconnecting..."
forkClient app
Right (Just x) -> do debugRecv x
parseMessage x
where
parseMessage "" = return ()
parseMessage m = do
let d = decodeOrFail @IB_IN (LBS.fromStrict m)
case d of
Right (rest, offset, result) -> do
atomically $ writeTQueue toRecieve (Msg_IB_IN result)
parseMessage (LBS.toStrict rest)
Left (rest, offset, err) -> do
run $ logInfo (display $ T.pack $ "Could not understand message: "<> ppShow err <> " ... skipping.\nRAW: " <> show m)
if m == "\NUL" then do
run $ logInfo (display $ T.pack "killing NUL")
parseMessage (LBS.toStrict $ LBS.tail rest)
else
parseMessage (LBS.toStrict rest)
go False
go False
recvAll :: Socket -> IO (Maybe ByteString)
recvAll s = do
d <- recv s 4096
let l = BS.length d
if
| l == 0 -> return Nothing
| l < 4096 -> return $ Just d
| l == 4096 -> do
next <- recvAll s
return $ (d<>) <$> next
| otherwise -> error "recvAll: recv got more bytes then requested. Impossible according to RFC"

581
src/IBClient/Types.hs Normal file
View File

@ -0,0 +1,581 @@
{-# HLINT ignore "Use camelCase" #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# OPTIONS_GHC -Wno-partial-fields #-}
module IBClient.Types where
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.Text.Encoding
import Data.Maybe
import Data.Default
import Data.Aeson (FromJSON, ToJSON)
import GHC.Enum (Enum(..))
import RIO
import RIO.List
import RIO.List.Partial
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.Char8 as LBS8
import qualified Debug.Trace as D
data Msg_IB_OUT = IBDisconnect
| Msg_IB_OUT IB_OUT
deriving (Show, Eq)
data Msg_IB_IN = IBServerGone
| Msg_IB_IN IB_IN
deriving (Show, Eq)
data IBTypes = IBString ByteString
| IBBool Bool
| IBArray [ByteString]
deriving (Show, Eq)
toBS :: IBTypes -> ByteString
toBS (IBString t) = t
toBS (IBArray a) = (BS8.pack . show . length $ a) <> BS.intercalate "\0" a <> "\0"
toBS (IBBool True) = "1"--BS.pack [0,0,0,1] -- bool == 32-bit int in IB
toBS (IBBool False) = "0"--BS.pack [0,0,0,0] -- bool == 32-bit int in IB
newtype IBGenericMessage = IBGenericMessage
{ fields :: [IBTypes]
} deriving (Show, Eq)
instance Binary IBGenericMessage where
put (IBGenericMessage f) = do
let msg = BS.intercalate "\0" (toBS <$> f) <> "\0"
putWord32be . fromIntegral . BS.length $ msg
putByteString msg
get = do
len <- getWord32be
D.traceShow len $ return ()
fields <- BS.split 0 . BS.init <$> getByteString (fromIntegral len)
D.traceShow fields $ return ()
return $ IBGenericMessage $ IBString <$> fields
data IB_OUT = IB_StartAPI { version :: Text, clientId :: Text }
| IB_RequestPositions
| IB_RequestAccountData { subscribe :: Bool, acctCode :: Text }
| IB_RequestMatchingSymbol { reqId :: Int, symbol :: Text }
| IB_RequestMarketDataType { dataType :: IBMarketDataType }
| IB_RequestMktData { tickerId :: Int, contract :: IBContract, genericTickList :: Text, snapshot :: Bool, regulatorySnapshot :: Bool }
deriving (Show, Eq)
tToIB :: Text -> IBTypes
tToIB = IBString . encodeUtf8
iToIB :: Int -> IBTypes
iToIB = IBString . BS8.pack . show
fToIB :: Float -> IBTypes
fToIB = IBString . BS8.pack . show
clToIB :: [IBComboLeg] -> [IBTypes]
clToIB as = iToIB (length as) : concatMap (\IBComboLeg{..} -> [iToIB conId, iToIB ratio, tToIB action, tToIB exchange]) as
dncToIB :: Maybe IBDeltaNeutralContract -> [IBTypes]
dncToIB Nothing = []
dncToIB (Just IBDeltaNeutralContract{..}) = [iToIB 1, iToIB conId, fToIB delta, fToIB price]
instance Binary IB_OUT where
put (IB_StartAPI v c) = put (IBGenericMessage [IBString "71", tToIB v, tToIB c, IBString ""])
put IB_RequestPositions = put (IBGenericMessage [IBString "61", IBString "v"])
put (IB_RequestAccountData s a) = put (IBGenericMessage [IBString "6", IBString "2", IBBool s, tToIB a])
put (IB_RequestMatchingSymbol i s) = put (IBGenericMessage [IBString "81", iToIB i, tToIB s])
put (IB_RequestMktData t IBContract{..} l s r) = put $ D.traceShowId (IBGenericMessage $ [IBString "1", IBString "11", iToIB t, iToIB conId, tToIB symbol, tToIB secType, tToIB lastTradeDate, fToIB strike, tToIB right, tToIB multiplier, tToIB exchange, tToIB primaryExchange, tToIB currency, tToIB localSymbol, tToIB tradingClass] <> clToIB comboLegs <> dncToIB deltaNeutralContract <> [tToIB l, iToIB (if s then 1 else 0), iToIB (if r then 1 else 0), tToIB ""])
put (IB_RequestMarketDataType t) = put (IBGenericMessage [IBString "59", IBString "1", iToIB $ fromEnum t])
get = do f <- fmap (\(IBString x) -> x) . fields <$> get
case headMaybe f of
Just "71" -> return $ IB_StartAPI (decodeUtf8 $ f!!1) (decodeUtf8 $ f!!2)
Just "6" -> return $ IB_RequestAccountData {- ignore version -} (all (==0) . BS.unpack $ f!!2) (decodeUtf8 $ f!!3)
Just "59" -> return $ IB_RequestMarketDataType {- ignore version -} (toEnum . fromJust . readMaybe . BS8.unpack $ f!!2)
Just x -> fail $ "unkonwn IB_OUT type" <> BS8.unpack x
Nothing -> fail $ "No Fields"
data IBContract = IBContract
{ conId :: Int
, symbol :: Text
, secType :: Text
, lastTradeDate :: Text
, strike :: Float
, right :: Text
, multiplier :: Text
, exchange :: Text -- ^ can be SMART
, primaryExchange :: Text -- ^ actual exchange - MUST NOT BE SMART
, currency :: Text
, localSymbol :: Text
, tradingClass :: Text
, includeExpired :: Bool
, secIdType :: Text
, secId :: Text
, comboLegsDescrip :: Text -- ^ received in open order 14 and up for all combos
, comboLegs :: [IBComboLeg]
, deltaNeutralContract :: Maybe IBDeltaNeutralContract
} deriving (Show, Eq, Generic)
deriving anyclass (FromJSON, ToJSON)
instance Default IBContract where
def = IBContract 0 "" "" "" 0 "" "" "" "" "" "" "" False "" "" "" [] Nothing
data IBComboLeg = IBComboLeg
{ conId :: Int
, ratio :: Int
, action :: Text -- ^ BUY/SELL/SSHORT
, exchange :: Text
, openClose :: Int -- ^ LegOpenClose enum values
, shortSaleSlot :: Int
, designatedLocation :: Text
, exemptCode :: Int
} deriving (Show, Eq, Generic)
deriving anyclass (FromJSON, ToJSON)
instance Default IBComboLeg where
def = IBComboLeg 0 0 "" "" 0 0 "" (negate 1)
data IBDeltaNeutralContract = IBDeltaNeutralContract
{ conId :: Int
, delta :: Float
, price :: Float
} deriving (Show, Eq, Generic)
deriving anyclass (FromJSON, ToJSON)
instance Default IBDeltaNeutralContract where
def = IBDeltaNeutralContract 0 0 0
data IBMarketDataType = RealTime
| Frozen
| Delayed
| DelayedFrozen
deriving (Show, Eq, Generic)
deriving anyclass (FromJSON, ToJSON)
instance Default IBMarketDataType where
def = DelayedFrozen
instance Enum IBMarketDataType where
toEnum 1 = RealTime
toEnum 2 = Frozen
toEnum 3 = Delayed
toEnum 4 = DelayedFrozen
toEnum _ = def
fromEnum RealTime = 1
fromEnum Frozen = 2
fromEnum Delayed = 3
fromEnum DelayedFrozen = 4
data IBTickType = Unknown Int
| IBTickType_Bid_Size
| IBTickType_Bid_Price
| IBTickType_Ask_Price
| IBTickType_Ask_Size
| IBTickType_Last_Price
| IBTickType_Last_Size
| IBTickType_High
| IBTickType_Low
| IBTickType_Volume
| IBTickType_Close_Price
| IBTickType_Bid_Option_Computation
| IBTickType_Ask_Option_Computation
| IBTickType_Last_Option_Computation
| IBTickType_Model_Option_Computation
| IBTickType_Open_Tick
| IBTickType_Low_13_Weeks
| IBTickType_High_13_Weeks
| IBTickType_Low_26_Weeks
| IBTickType_High_26_Weeks
| IBTickType_Low_52_Weeks
| IBTickType_High_52_Weeks
| IBTickType_Average_Volume
| IBTickType_Open_Interest
| IBTickType_Option_Historical_Volatility
| IBTickType_Option_Implied_Volatility
| IBTickType_Option_Bid_Exchange
| IBTickType_Option_Ask_Exchange
| IBTickType_Option_Call_Open_Interest
| IBTickType_Option_Put_Open_Interest
| IBTickType_Option_Call_Volume
| IBTickType_Option_Put_Volume
| IBTickType_Index_Future_Premium
| IBTickType_Bid_Exchange
| IBTickType_Ask_Exchange
| IBTickType_Auction_Volume
| IBTickType_Auction_Price
| IBTickType_Auction_Imbalance
| IBTickType_Mark_Price
| IBTickType_Bid_EFP_Computation
| IBTickType_Ask_EFP_Computation
| IBTickType_Last_EFP_Computation
| IBTickType_Open_EFP_Computation
| IBTickType_High_EFP_Computation
| IBTickType_Low_EFP_Computation
| IBTickType_Close_EFP_Computation
| IBTickType_Last_Timestamp
| IBTickType_Shortable
| IBTickType_RT_Volume
| IBTickType_Halted
| IBTickType_Bid_Yield
| IBTickType_Ask_Yield
| IBTickType_Last_Yield
| IBTickType_Custom_Option_Computation
| IBTickType_Trade_Count
| IBTickType_Trade_Rate
| IBTickType_Volume_Rate
| IBTickType_Last_RTH_Trade
| IBTickType_RT_Historical_Volatility
| IBTickType_IB_Dividends
| IBTickType_Bond_Factor_Multiplier
| IBTickType_Regulatory_Imbalance
| IBTickType_News
| IBTickType_ShortTerm_Volume_3_Minutes
| IBTickType_ShortTerm_Volume_5_Minutes
| IBTickType_ShortTerm_Volume_10_Minutes
| IBTickType_Delayed_Bid
| IBTickType_Delayed_Ask
| IBTickType_Delayed_Last
| IBTickType_Delayed_Bid_Size
| IBTickType_Delayed_Ask_Size
| IBTickType_Delayed_Last_Size
| IBTickType_Delayed_High_Price
| IBTickType_Delayed_Low_Price
| IBTickType_Delayed_Volume
| IBTickType_Delayed_Close
| IBTickType_Delayed_Open
| IBTickType_RT_Trade_Volume
| IBTickType_Creditman_mark_price
| IBTickType_Creditman_slow_mark_price
| IBTickType_Delayed_Bid_Option
| IBTickType_Delayed_Ask_Option
| IBTickType_Delayed_Last_Option
| IBTickType_Delayed_Model_Option
| IBTickType_Last_Exchange
| IBTickType_Last_Regulatory_Time
| IBTickType_Futures_Open_Interest
| IBTickType_Average_Option_Volume
| IBTickType_Delayed_Last_Timestamp
| IBTickType_Shortable_Shares
| IBTickType_ETF_Nav_Close
| IBTickType_ETF_Nav_Prior_Close
| IBTickType_ETF_Nav_Bid
| IBTickType_ETF_Nav_Ask
| IBTickType_ETF_Nav_Last
| IBTickType_ETF_Nav_Frozen_Last
| IBTickType_ETF_Nav_High
| IBTickType_ETF_Nav_Low
deriving (Show, Eq)
instance Enum IBTickType where
toEnum 0 = IBTickType_Bid_Size
toEnum 1 = IBTickType_Bid_Price
toEnum 2 = IBTickType_Ask_Price
toEnum 3 = IBTickType_Ask_Size
toEnum 4 = IBTickType_Last_Price
toEnum 5 = IBTickType_Last_Size
toEnum 6 = IBTickType_High
toEnum 7 = IBTickType_Low
toEnum 8 = IBTickType_Volume
toEnum 9 = IBTickType_Close_Price
toEnum 10 = IBTickType_Bid_Option_Computation
toEnum 11 = IBTickType_Ask_Option_Computation
toEnum 12 = IBTickType_Last_Option_Computation
toEnum 13 = IBTickType_Model_Option_Computation
toEnum 14 = IBTickType_Open_Tick
toEnum 15 = IBTickType_Low_13_Weeks
toEnum 16 = IBTickType_High_13_Weeks
toEnum 17 = IBTickType_Low_26_Weeks
toEnum 18 = IBTickType_High_26_Weeks
toEnum 19 = IBTickType_Low_52_Weeks
toEnum 20 = IBTickType_High_52_Weeks
toEnum 21 = IBTickType_Average_Volume
toEnum 22 = IBTickType_Open_Interest
toEnum 23 = IBTickType_Option_Historical_Volatility
toEnum 24 = IBTickType_Option_Implied_Volatility
toEnum 25 = IBTickType_Option_Bid_Exchange
toEnum 26 = IBTickType_Option_Ask_Exchange
toEnum 27 = IBTickType_Option_Call_Open_Interest
toEnum 28 = IBTickType_Option_Put_Open_Interest
toEnum 29 = IBTickType_Option_Call_Volume
toEnum 30 = IBTickType_Option_Put_Volume
toEnum 31 = IBTickType_Index_Future_Premium
toEnum 32 = IBTickType_Bid_Exchange
toEnum 33 = IBTickType_Ask_Exchange
toEnum 34 = IBTickType_Auction_Volume
toEnum 35 = IBTickType_Auction_Price
toEnum 36 = IBTickType_Auction_Imbalance
toEnum 37 = IBTickType_Mark_Price
toEnum 38 = IBTickType_Bid_EFP_Computation
toEnum 39 = IBTickType_Ask_EFP_Computation
toEnum 40 = IBTickType_Last_EFP_Computation
toEnum 41 = IBTickType_Open_EFP_Computation
toEnum 42 = IBTickType_High_EFP_Computation
toEnum 43 = IBTickType_Low_EFP_Computation
toEnum 44 = IBTickType_Close_EFP_Computation
toEnum 45 = IBTickType_Last_Timestamp
toEnum 46 = IBTickType_Shortable
toEnum 48 = IBTickType_RT_Volume
toEnum 49 = IBTickType_Halted
toEnum 50 = IBTickType_Bid_Yield
toEnum 51 = IBTickType_Ask_Yield
toEnum 52 = IBTickType_Last_Yield
toEnum 53 = IBTickType_Custom_Option_Computation
toEnum 54 = IBTickType_Trade_Count
toEnum 55 = IBTickType_Trade_Rate
toEnum 56 = IBTickType_Volume_Rate
toEnum 57 = IBTickType_Last_RTH_Trade
toEnum 58 = IBTickType_RT_Historical_Volatility
toEnum 59 = IBTickType_IB_Dividends
toEnum 60 = IBTickType_Bond_Factor_Multiplier
toEnum 61 = IBTickType_Regulatory_Imbalance
toEnum 62 = IBTickType_News
toEnum 63 = IBTickType_ShortTerm_Volume_3_Minutes
toEnum 64 = IBTickType_ShortTerm_Volume_5_Minutes
toEnum 65 = IBTickType_ShortTerm_Volume_10_Minutes
toEnum 66 = IBTickType_Delayed_Bid
toEnum 67 = IBTickType_Delayed_Ask
toEnum 68 = IBTickType_Delayed_Last
toEnum 69 = IBTickType_Delayed_Bid_Size
toEnum 70 = IBTickType_Delayed_Ask_Size
toEnum 71 = IBTickType_Delayed_Last_Size
toEnum 72 = IBTickType_Delayed_High_Price
toEnum 73 = IBTickType_Delayed_Low_Price
toEnum 74 = IBTickType_Delayed_Volume
toEnum 75 = IBTickType_Delayed_Close
toEnum 76 = IBTickType_Delayed_Open
toEnum 77 = IBTickType_RT_Trade_Volume
toEnum 78 = IBTickType_Creditman_mark_price
toEnum 79 = IBTickType_Creditman_slow_mark_price
toEnum 80 = IBTickType_Delayed_Bid_Option
toEnum 81 = IBTickType_Delayed_Ask_Option
toEnum 82 = IBTickType_Delayed_Last_Option
toEnum 83 = IBTickType_Delayed_Model_Option
toEnum 84 = IBTickType_Last_Exchange
toEnum 85 = IBTickType_Last_Regulatory_Time
toEnum 86 = IBTickType_Futures_Open_Interest
toEnum 87 = IBTickType_Average_Option_Volume
toEnum 88 = IBTickType_Delayed_Last_Timestamp
toEnum 89 = IBTickType_Shortable_Shares
toEnum 92 = IBTickType_ETF_Nav_Close
toEnum 93 = IBTickType_ETF_Nav_Prior_Close
toEnum 94 = IBTickType_ETF_Nav_Bid
toEnum 95 = IBTickType_ETF_Nav_Ask
toEnum 96 = IBTickType_ETF_Nav_Last
toEnum 97 = IBTickType_ETF_Nav_Frozen_Last
toEnum 98 = IBTickType_ETF_Nav_High
toEnum 99 = IBTickType_ETF_Nav_Low
toEnum x = D.trace ("Unknown tick-type-id: " <> show x) $ Unknown x
fromEnum (Unknown x) = x
fromEnum IBTickType_Bid_Size = 0
fromEnum IBTickType_Bid_Price = 1
fromEnum IBTickType_Ask_Price = 2
fromEnum IBTickType_Ask_Size = 3
fromEnum IBTickType_Last_Price = 4
fromEnum IBTickType_Last_Size = 5
fromEnum IBTickType_High = 6
fromEnum IBTickType_Low = 7
fromEnum IBTickType_Volume = 8
fromEnum IBTickType_Close_Price = 9
fromEnum IBTickType_Bid_Option_Computation = 10
fromEnum IBTickType_Ask_Option_Computation = 11
fromEnum IBTickType_Last_Option_Computation = 12
fromEnum IBTickType_Model_Option_Computation = 13
fromEnum IBTickType_Open_Tick = 14
fromEnum IBTickType_Low_13_Weeks = 15
fromEnum IBTickType_High_13_Weeks = 16
fromEnum IBTickType_Low_26_Weeks = 17
fromEnum IBTickType_High_26_Weeks = 18
fromEnum IBTickType_Low_52_Weeks = 19
fromEnum IBTickType_High_52_Weeks = 20
fromEnum IBTickType_Average_Volume = 21
fromEnum IBTickType_Open_Interest = 22
fromEnum IBTickType_Option_Historical_Volatility = 23
fromEnum IBTickType_Option_Implied_Volatility = 24
fromEnum IBTickType_Option_Bid_Exchange = 25
fromEnum IBTickType_Option_Ask_Exchange = 26
fromEnum IBTickType_Option_Call_Open_Interest = 27
fromEnum IBTickType_Option_Put_Open_Interest = 28
fromEnum IBTickType_Option_Call_Volume = 29
fromEnum IBTickType_Option_Put_Volume = 30
fromEnum IBTickType_Index_Future_Premium = 31
fromEnum IBTickType_Bid_Exchange = 32
fromEnum IBTickType_Ask_Exchange = 33
fromEnum IBTickType_Auction_Volume = 34
fromEnum IBTickType_Auction_Price = 35
fromEnum IBTickType_Auction_Imbalance = 36
fromEnum IBTickType_Mark_Price = 37
fromEnum IBTickType_Bid_EFP_Computation = 38
fromEnum IBTickType_Ask_EFP_Computation = 39
fromEnum IBTickType_Last_EFP_Computation = 40
fromEnum IBTickType_Open_EFP_Computation = 41
fromEnum IBTickType_High_EFP_Computation = 42
fromEnum IBTickType_Low_EFP_Computation = 43
fromEnum IBTickType_Close_EFP_Computation = 44
fromEnum IBTickType_Last_Timestamp = 45
fromEnum IBTickType_Shortable = 46
fromEnum IBTickType_RT_Volume = 48
fromEnum IBTickType_Halted = 49
fromEnum IBTickType_Bid_Yield = 50
fromEnum IBTickType_Ask_Yield = 51
fromEnum IBTickType_Last_Yield = 52
fromEnum IBTickType_Custom_Option_Computation = 53
fromEnum IBTickType_Trade_Count = 54
fromEnum IBTickType_Trade_Rate = 55
fromEnum IBTickType_Volume_Rate = 56
fromEnum IBTickType_Last_RTH_Trade = 57
fromEnum IBTickType_RT_Historical_Volatility = 58
fromEnum IBTickType_IB_Dividends = 59
fromEnum IBTickType_Bond_Factor_Multiplier = 60
fromEnum IBTickType_Regulatory_Imbalance = 61
fromEnum IBTickType_News = 62
fromEnum IBTickType_ShortTerm_Volume_3_Minutes = 63
fromEnum IBTickType_ShortTerm_Volume_5_Minutes = 64
fromEnum IBTickType_ShortTerm_Volume_10_Minutes = 65
fromEnum IBTickType_Delayed_Bid = 66
fromEnum IBTickType_Delayed_Ask = 67
fromEnum IBTickType_Delayed_Last = 68
fromEnum IBTickType_Delayed_Bid_Size = 69
fromEnum IBTickType_Delayed_Ask_Size = 70
fromEnum IBTickType_Delayed_Last_Size = 71
fromEnum IBTickType_Delayed_High_Price = 72
fromEnum IBTickType_Delayed_Low_Price = 73
fromEnum IBTickType_Delayed_Volume = 74
fromEnum IBTickType_Delayed_Close = 75
fromEnum IBTickType_Delayed_Open = 76
fromEnum IBTickType_RT_Trade_Volume = 77
fromEnum IBTickType_Creditman_mark_price = 78
fromEnum IBTickType_Creditman_slow_mark_price = 79
fromEnum IBTickType_Delayed_Bid_Option = 80
fromEnum IBTickType_Delayed_Ask_Option = 81
fromEnum IBTickType_Delayed_Last_Option = 82
fromEnum IBTickType_Delayed_Model_Option = 83
fromEnum IBTickType_Last_Exchange = 84
fromEnum IBTickType_Last_Regulatory_Time = 85
fromEnum IBTickType_Futures_Open_Interest = 86
fromEnum IBTickType_Average_Option_Volume = 87
fromEnum IBTickType_Delayed_Last_Timestamp = 88
fromEnum IBTickType_Shortable_Shares = 89
fromEnum IBTickType_ETF_Nav_Close = 92
fromEnum IBTickType_ETF_Nav_Prior_Close = 93
fromEnum IBTickType_ETF_Nav_Bid = 94
fromEnum IBTickType_ETF_Nav_Ask = 95
fromEnum IBTickType_ETF_Nav_Last = 96
fromEnum IBTickType_ETF_Nav_Frozen_Last = 97
fromEnum IBTickType_ETF_Nav_High = 98
fromEnum IBTickType_ETF_Nav_Low = 99
type IB_DerivativeSecType = Text
data IB_SymbolSample = IB_SymbolSample
{ symId :: Int
, symbol :: Text
, secType :: Text
, primaryExchange :: Text
, currency :: Text
, derivatives :: [IB_DerivativeSecType]
} deriving (Show, Eq)
data IB_IN = IB_PositionData
| IB_ManagedAccts { accounts :: [Text] }
| IB_NextValidID { orderID :: Int }
| IB_ErrorMsg { errorID :: Int, errorCode :: Int, errorMsg :: Text }
| IB_AccountValue { key :: Text, value :: Text, currency :: Text, accountName :: Text }
| IB_AccountUpdateTime { time :: Text }
| IB_PortfolioValue { contract :: IBContract, position :: Float, marketPrice :: Float, marketValue :: Float, averageCost :: Float, unrealizedPNL :: Float, realizedPNL :: Float, accountName :: Text }
| IB_SymbolSamples { nextId :: Int, samples :: [IB_SymbolSample] }
| IB_MarketDataType { tickerId :: Int, dataType :: IBMarketDataType }
| IB_TickReqParams { tickerId :: Int, minTick :: Float, bboExchange :: Text, snapshotPermissions :: Int }
| IB_TickPrice { tickerId :: Int, tickType :: IBTickType, price :: Float, size :: Int, attrMask :: Int }
| IB_TickSize { tickerId :: Int, fieldId :: Int, size :: Int } -- TODO: field is an enum
| IB_TickString { tickerId :: Int, tickType :: IBTickType, content :: Text }
deriving (Show, Eq)
voidVersion :: LBS.ByteString -> LBS.ByteString -> Get ()
voidVersion t v = do
version <- getLazyByteStringNul
when (version /= v) $ D.trace ("Unexpected Version '" <> LBS8.unpack version <> "' for Message-Type " <> LBS8.unpack t <> ". Expected: '" <> LBS8.unpack v <> "'.") (return ())
instance Binary IB_IN where
put (IB_ErrorMsg i c m) = put (IBGenericMessage [IBString "4", IBString "2", IBString $ BS8.pack $ show i, IBString $ BS8.pack $ show c, tToIB m])
put (IB_AccountValue k v c n) = put (IBGenericMessage [IBString "6", IBString "2", tToIB k, tToIB v, tToIB c, tToIB n])
put (IB_PortfolioValue IBContract{..} p pp v c u r n) = put $ IBGenericMessage [ IBString "7", IBString "8" -- id/version
, iToIB conId, tToIB symbol, tToIB secType, tToIB lastTradeDate, fToIB strike, tToIB right, tToIB multiplier, tToIB primaryExchange, tToIB currency, tToIB localSymbol, tToIB tradingClass -- contract
, fToIB p, fToIB pp, fToIB v, fToIB c, fToIB u, fToIB r, tToIB n
]
put (IB_AccountUpdateTime t) = put (IBGenericMessage [IBString "8", IBString "1", tToIB t])
put (IB_NextValidID v) = put (IBGenericMessage [IBString "9", IBString "1", IBString $ BS8.pack $ show v])
put (IB_ManagedAccts a) = put (IBGenericMessage [IBString "15", IBArray $ encodeUtf8 <$> a])
put IB_PositionData = put (IBGenericMessage [IBString "61"])
put IB_SymbolSamples{} = error "not implemented"
put IB_MarketDataType{} = error "not implemented"
put IB_TickReqParams{} = error "not implemented"
put IB_TickPrice{} = error "not implemented"
put IB_TickSize{} = error "not implemented"
put IB_TickString{} = error "not implemented"
--put (IB_SymbolSamples r s) = put (IBGenericMessage [IBString "79", IBString "1", iToIB r, IBArray $ s]) TODO: FIXME
get = do
msglen <- getWord32be
when (msglen == 0) $ fail "empty message"
ident <- return <$> getLazyByteStringNul
case ident of
Just "1" -> do
voidVersion "1" "6"
IB_TickPrice <$> ib2int <*> (toEnum <$> ib2int) <*> ib2f <*> ib2int <*> ib2int
Just "2" -> do
voidVersion "2" "6"
IB_TickSize <$> ib2int <*> ib2int <*> ib2int
Just "4" -> do
voidVersion "4" "2"
IB_ErrorMsg <$> ib2int <*> ib2int <*> ib2txt
Just "6" -> do
voidVersion "6" "2"
IB_AccountValue <$> ib2txt <*> ib2txt <*> ib2txt <*> ib2txt
Just "7" -> do
voidVersion "7" "8"
c <- IBContract <$> ib2int <*> ib2txt <*> ib2txt <*> ib2txt <*> ib2f <*> ib2txt <*> ib2txt <*> pure "" <*> ib2txt <*> ib2txt <*> ib2txt <*> ib2txt
IB_PortfolioValue (c False "" "" "" [] Nothing) <$> ib2f <*> ib2f <*> ib2f <*> ib2f <*> ib2f <*> ib2f <*> ib2txt
Just "8" -> do
voidVersion "8" "1"
IB_AccountUpdateTime <$> ib2txt
Just "9" -> do
voidVersion "9" "1"
IB_NextValidID <$> ib2int
Just "15" -> do
len <- ib2int
IB_ManagedAccts <$> forM [1..len] (const ib2txt)
Just "46" -> do
voidVersion "46" "6"
IB_TickString <$> ib2int <*> (toEnum <$> ib2int) <*> ib2txt
Just "58" -> do
voidVersion "58" "1"
IB_MarketDataType <$> ib2int <*> (toEnum <$> ib2int)
Just "61" -> return IB_PositionData
Just "79" -> do
reqId <- ib2int
len <- ib2int
symsamples <- forM [1..len] $ const $ do
f <- IB_SymbolSample <$> ib2int <*> ib2txt <*> ib2txt <*> ib2txt <*> ib2txt
n <- ib2int
derivatives <- forM [1..n] $ const ib2txt
return $ f derivatives
return $ IB_SymbolSamples reqId symsamples
Just "81" -> do
IB_TickReqParams <$> ib2int <*> ib2f <*> ib2txt <*> ib2int
Just x -> do
payload <- getByteString (fromIntegral msglen - (if null ident then 0 else length ident + 1) - 1) -- drop rest of message
D.trace ("Payload for "<> LBS8.unpack x <> " not understood: " <> show (IBGenericMessage $ fmap IBString . BS.split 0 . BS.init $ payload)) $ return ()
fail $ "unkonwn IB_IN type " <> LBS8.unpack x
Nothing -> fail "Cannot decode Message: no identifier"
ib2int :: Get Int
ib2int = fromMaybe (-1) . readMaybe . LBS8.unpack <$> getLazyByteStringNul
ib2f :: Get Float
ib2f = fromMaybe (-1) . readMaybe . LBS8.unpack <$> getLazyByteStringNul
ib2txt :: Get Text
ib2txt = decodeUtf8 . LBS.toStrict <$> getLazyByteStringNul

16
src/Import.hs Normal file
View File

@ -0,0 +1,16 @@
{-# LANGUAGE NoImplicitPrelude #-}
module Import
( module RIO
, module Types
, module Data.Aeson
, module Data.Default
, module Text.Show.Pretty
, module IBClient.Types
) where
import RIO
import Types
import Data.Aeson (FromJSON, ToJSON)
import Data.Default
import Text.Show.Pretty
import IBClient.Types

230
src/Run.hs Normal file
View File

@ -0,0 +1,230 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Run (run) where
import Import
import Chart
import Types
import Control.Concurrent
import Data.Aeson (encodeFile)
import Data.Bits
import Data.FingerTree (ViewL(..), ViewR(..), viewl, viewr, split, FingerTree)
import DearImGui
import DearImGui.OpenGL3
import DearImGui.GLFW
import Graphics.GL
import Data.StateVar
import qualified Graphics.UI.GLFW as GLFW
import qualified Data.Text as T
import qualified Data.HashMap.Strict as HM
import qualified Data.FingerTree as FT
import IBClient.Connection
run :: RIO App ()
run = do
-- set up IB connection & start threads feeding stuff
renderLoop
-- close connections to IB
renderLoop :: RIO App ()
renderLoop = do
win <- appWindow <$> ask
liftIO GLFW.pollEvents
close <- liftIO $ GLFW.windowShouldClose win
if close
then do
-- save settings & config
(w,h) <- liftIO $ GLFW.getWindowSize win
settings <- appSettings <$> ask
refs <- appRefs <$> ask
host' <- liftIO . readTVarIO . twsConnectionRefsHost . twsConnectionRefs $ refs
port' <- liftIO . readTVarIO . twsConnectionRefsPort . twsConnectionRefs $ refs
let settings' = settings & windowParams . windowWidth .~ w
& windowParams . windowHeight .~ h
& twsConnection . host .~ T.pack host'
& twsConnection . port .~ T.pack port'
liftIO $ encodeFile "settings.json" settings'
logInfo $ display ("Settings Saved" :: Text)
-- save cached data
logInfo $ display $ T.pack $ ppShow settings'
else do
refs' <- appRefs <$> ask
data' <- appData <$> ask
selectedAccount <- readTVarIO $ currentAccount refs'
let sendQ = twsConnectionSend $ twsConnectionRefs refs'
-- Tell ImGui we're starting a new frame
liftIO $ do
openGL3NewFrame
glfwNewFrame
newFrame
-- Menu bar
withMainMenuBarOpen $ do
withMenuOpen "File" $ do
menuItem "Quit" >>= \case
False -> return ()
True -> liftIO $ GLFW.setWindowShouldClose win True
let cr = twsConnectionRefs refs'
accs <- fmap HM.keys $ liftIO $ readTVarIO $ Types.accounts data'
withComboOpen "Account" (maybe "Select account" T.unpack selectedAccount) $ do
forM_ accs $ \a -> do
selectable (T.unpack a) >>= \case
False -> return ()
True -> do
-- cancel subscription of old account (if any)
readTVarIO (currentAccount refs') >>= \case
Nothing -> return ()
Just aid -> liftIO $ atomically $ writeTQueue sendQ $ Msg_IB_OUT $ IB_RequestAccountData False aid
-- subscribe to new account
liftIO $ atomically $ writeTQueue sendQ $ Msg_IB_OUT $ IB_RequestAccountData True a
-- finally change
liftIO $ atomically $ modifyTVar' (currentAccount refs') (const $ Just a)
let cStatus = twsConnectionStatus cr
connHost <- liftIO $ readTVarIO $ twsConnectionRefsHost cr
connPort <- liftIO $ readTVarIO $ twsConnectionRefsPort cr
connStatus <- liftIO $ readTVarIO cStatus
when (connStatus == TWSDisconnected) $ button "Connect" >>= \case
False -> return ()
True -> do
if connStatus == TWSDisconnected then do
logDebug $ display ("Connecting to TWS on " <> T.pack connHost <> ":" <> T.pack connPort <> "." :: Text)
app <- ask
void $ liftIO $ forkIO $ forkClient app
else do
logInfo $ display ("Tried to connect, but we are connected" :: Text)
return ()
cStatusText <- liftIO $ get (InjetiveGettable cStatus :: InjetiveGettable TWSConnectionStatus String)
textColored (InjetiveGettable cStatus :: InjetiveGettable TWSConnectionStatus ImVec4) cStatusText
bracket_ (begin "TWS-Connection") end $ do
let cr = twsConnectionRefs refs'
let cStatus = twsConnectionStatus cr
let cHost = twsConnectionRefsHost cr
let cPort = twsConnectionRefsPort cr
void $ inputText "Host" cHost 255
void $ inputText "Port" cPort 255
button "Connect" >>= \case
False -> return ()
True -> do
connStatus <- liftIO $ readTVarIO cStatus
connHost <- liftIO $ readTVarIO cHost
connPort <- liftIO $ readTVarIO cPort
if connStatus == TWSDisconnected then do
logDebug $ display ("Connecting to TWS on " <> T.pack connHost <> ":" <> T.pack connPort <> "." :: Text)
app <- ask
void $ liftIO $ forkIO $ forkClient app
else do
logInfo $ display ("Tried to connect, but we are connected" :: Text)
return ()
-- TODO: show connection-status
cStatusText <- liftIO $ get (InjetiveGettable cStatus :: InjetiveGettable TWSConnectionStatus String)
textColored (InjetiveGettable cStatus :: InjetiveGettable TWSConnectionStatus ImVec4) cStatusText
bracket_ (begin "Portfolio") end $ do
readTVarIO (currentAccount refs') >>= \case
Nothing -> text "No account selected"
Just aid -> do
accs <- liftIO $ readTVarIO $ Types.accounts data'
withTable defTableOptions "Portfolio" 6 $ \case
False -> return ()
True -> do
tableSetupColumn "Symbol"
tableSetupColumn "Position"
tableSetupColumn "Unrealized Profit"
tableSetupColumn "Realized Profit"
tableSetupColumn "AVG"
tableSetupColumn "Market Value"
tableHeadersRow
forM_ ((accs HM.! aid) ^.. accountPortfolio . traverse) $ \(IBPortfolioValue c p mp mv ac up rp) ->
do
tableNextRow
whenM tableNextColumn (text $ T.unpack $ localSymbol c)
whenM tableNextColumn (text $ show p)
whenM tableNextColumn (text $ show up)
whenM tableNextColumn (text $ show rp)
whenM tableNextColumn (text $ show mp)
whenM tableNextColumn (text $ show mv)
bracket_ (begin "Search Symbols") end $ do
readTVarIO (currentAccount refs') >>= \case
Nothing -> text "No account selected"
Just _ -> do
let nextIDVar = nextValidID data'
sLookup = nextSymbolLookup data'
readTVarIO nextIDVar >>= \case
Nothing -> text "no id available, waiting ..."
Just i -> do
void $ inputText "Symbol-Lookup" (InjetiveGettable @Text @String sLookup) 255
button "Lookup" >>= \case
False -> return ()
True ->
liftIO $ atomically $ do
readTVar sLookup >>= writeTQueue sendQ . Msg_IB_OUT . IB_RequestMatchingSymbol i
modifyTVar' nextIDVar (const Nothing)
withTable (defTableOptions { tableFlags = ImGuiTableFlags_SortMulti .|. ImGuiTableFlags_Sortable}) "Symbol" 5 $ \case
False -> return ()
True -> do
tableSetupColumn "Symbol"
tableSetupColumn "Security type"
tableSetupColumn "Primary exchange"
tableSetupColumn "Currency"
tableSetupColumn "Available derivatives"
withSortableTable $ \(mustSort, sortSpecs) -> do
when mustSort $ liftIO $ pPrint sortSpecs
tableHeadersRow
lResult <- readTVarIO $ symbolLookupResults data'
forM_ lResult $ \contract@IBSymbolSample{..} -> do
let popupName = "SymbolAction"<>show _symbolId
withPopup popupName $ \isPopupOpen -> do
when isPopupOpen $ do
button "creatChart" >>= \case
False -> return ()
True -> do
logInfo $ display $ "new chart open for: " <> _symbol
newChart $ def { conId = _symbolId, symbol = _symbol, secType = _secType, exchange = "SMART", primaryExchange = _primaryExchange, currency = _currency}
let printDatum x = whenM tableNextColumn $ text $ T.unpack x
tableNextRow
whenM tableNextColumn $ do
void $ selectableWith (defSelectableOptions { flags = ImGuiSelectableFlags_SpanAllColumns }) (T.unpack _symbol)
openPopupOnItemClick popupName ImGuiPopupFlags_MouseButtonRight
printDatum _secType
printDatum _primaryExchange
printDatum _currency
printDatum $ T.intercalate ", " _derivatives
-- chart windows
charts <- liftIO . readTVarIO . appCharts $ refs'
forM_ (HM.toList charts) $ \(symbol, cVar) -> do
bracket_ (begin (T.unpack symbol)) end $ do
Chart{..} <- liftIO . readTVarIO $ cVar
case viewr chartData of
EmptyR -> text "no last price"
(_ :> ChartPoint{..}) -> text $ "Last: " <> show pointValue <> "\nTime: " <> show timeOfDay
text $ ppShow chartCache
text $ ppShow lastCacheUpdate
return ()
-- Show the ImGui demo window
showDemoWindow
-- Show the ImPlot demo window
--showPlotDemoWindow
-- Render
liftIO $ glClear GL_COLOR_BUFFER_BIT
render
liftIO $ openGL3RenderDrawData =<< getDrawData
liftIO $ GLFW.swapBuffers win
renderLoop

240
src/Types.hs Normal file
View File

@ -0,0 +1,240 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Types where
import Data.Aeson hiding (Options)
import Data.Default
import Data.StateVar
import Data.Types.Injective
import Data.Time
import Data.FingerTree
import Data.Semigroup
import GHC.Generics
import Graphics.UI.GLFW (Window)
import DearImGui
import RIO
import RIO.Process
import Lens.Micro.TH
import qualified Data.Text as T
import IBClient.Types
-- | Command line arguments
data Options = Options
{ optionsVerbose :: !Bool
}
data WindowParams = WindowParams
{ _windowHeight :: Int
, _windowWidth :: Int
} deriving (Show, Generic, FromJSON, ToJSON)
instance Default WindowParams where
def = WindowParams 1024 768
data TWSConnection = TWSConnection
{ _host :: Text
, _port :: Text
} deriving (Show, Generic, FromJSON, ToJSON)
instance Default TWSConnection where
def = TWSConnection "127.0.0.1" "7497"
instance FromJSON LogLevel where
parseJSON = withText "LogLevel" $ \case
"LevelDebug" -> return LevelDebug
"LevelInfo" -> return LevelInfo
"LevelWarn" -> return LevelWarn
"LevelError" -> return LevelError
x -> fail $ T.unpack $ "encountered "<>x
instance ToJSON LogLevel where
toJSON LevelDebug = String "LevelDebug"
toJSON LevelInfo = String "LevelInfo"
toJSON LevelWarn = String "LevelWarn"
toJSON LevelError = String "LevelError"
toJSON (LevelOther _) = String "LevelDebug"
data Settings = Settings
{ _windowParams :: WindowParams
, _twsConnection :: TWSConnection
, _logLevel :: LogLevel
} deriving (Show, Generic, FromJSON, ToJSON)
makeLenses ''WindowParams
makeLenses ''TWSConnection
makeLenses ''Settings
instance Default Settings where
def = Settings def def LevelWarn
data TWSConnectionStatus = TWSDisconnected
| TWSConnecting
| TWSConnected
deriving (Show, Eq, Enum, Bounded)
data TWSConnectionRefs = TWSConnectionRefs
{ twsConnectionRefsHost :: TVar String
, twsConnectionRefsPort :: TVar String
, twsConnectionStatus :: TVar TWSConnectionStatus
, twsConnectionSend :: TQueue Msg_IB_OUT
, twsConnectionRecieve :: TQueue Msg_IB_IN
}
instance Injective TWSConnectionStatus ImVec4 where
to = \case
TWSDisconnected -> ImVec4 1 0 0 1
TWSConnecting -> ImVec4 1 1 0 1
TWSConnected -> ImVec4 0 1 0 1
instance Injective TWSConnectionStatus String where
to = \case
TWSDisconnected -> "Not Connected"
TWSConnecting -> "Trying to connect..."
TWSConnected -> "Connected"
data DataRefs = DataRefs
{ accounts :: TVar (HashMap Text IBAccount)
, nextValidID :: TVar (Maybe Int)
, nextSymbolLookup :: TVar Text
, symbolLookupResults :: TVar [IBSymbolSample]
}
mkIBAccount :: Text -> IBAccount
mkIBAccount u = IBAccount (IBAccountInfo u mempty mempty) mempty mempty
data IBAccount = IBAccount
{ _accountInfo :: IBAccountInfo
, _accountPortfolio :: [IBPortfolioValue]
, _accountStrategies :: [IBAccountStrategy]
} deriving (Show, Eq)
data IBAccountInfo = IBAccountInfo
{ _accountName :: Text
, _accountProperties :: HashMap Text [(Text, Text)] -- (value, currency)
, _accountLastUpdate :: Text
} deriving (Show, Eq)
data IBPortfolioValue = IBPortfolioValue
{ _contract :: IBContract
, _position :: Float
, _marketPrice :: Float
, _marketValue :: Float
, _averageCost :: Float
, _unrealizedPNL :: Float
, _realizedPNL :: Float
} deriving (Show, Eq)
newtype IBAccountStrategy = IBAccountStrategy [Int] -- Int -> contract-id for this strategy
deriving (Show, Eq)
data IBSymbolSample = IBSymbolSample
{ _symbolId :: Int
, _symbol :: Text
, _secType :: Text
, _primaryExchange :: Text
, _currency :: Text
, _derivatives :: [Text]
} deriving (Show, Eq)
makeLenses ''IBAccountStrategy
makeLenses ''IBAccountInfo
makeLenses ''IBAccount
data ChartSettings = ChartSettings
{ chartResolution :: Int
, chartStart :: Maybe UTCTime
, chartEnd :: Maybe UTCTime
} deriving (Show, Eq)
defChartSettings :: ChartSettings
defChartSettings = ChartSettings 60 Nothing Nothing
-- data TimeWindow = TimeWindow
-- { begin :: Int
-- , end :: Int
-- } deriving (Show, Eq)
--
-- instance Semigroup TimeWindow where
-- (TimeWindow a b) <> (TimeWindow x y) = TimeWindow (min a x) (max b y)
--
-- instance Monoid TimeWindow where
-- mempty = TimeWindow 0 86400
newtype TimePoint = TimePoint Int
deriving Eq
deriving newtype Show
deriving (Semigroup, Monoid) via (Max Int)
data ChartStudies = SMA { window :: Int, value :: Float }
| OLHC { olhc_open :: Float, olhc_low :: Float, olhc_high :: Float, olhc_close :: Float}
deriving (Show, Eq)
data ChartPoint = ChartPoint
{ timeOfDay :: TimePoint
, pointValue :: Float
, pointExtra :: [ChartStudies]
} deriving (Show, Eq)
instance Measured TimePoint ChartPoint where
measure = timeOfDay
data Chart = Chart
{ chartData :: FingerTree TimePoint ChartPoint
, chartHistData :: HashMap Day (FingerTree TimePoint ChartPoint)
, fillerThread :: ThreadId
, chartSettings :: ChartSettings
, chartCache :: [ChartPoint]
, lastCacheUpdate :: Maybe TimePoint
, chartDirty :: Bool
} deriving (Show, Eq)
newtype InjetiveGettable a b = InjetiveGettable
{ gettable :: TVar a
}
instance (Injective a b) => HasGetter (InjetiveGettable a b) b where
get r = liftIO $ do
(value :: a) <- get (gettable r)
return $ Data.Types.Injective.to value
instance (Injective b a) => HasSetter (InjetiveGettable a b) b where
t $= a = liftIO $ do
let b = Data.Types.Injective.to a
gettable t $= b
newtype DefaultJSON a = DefaultJSON { unDefaultJSON :: a }
instance (FromJSON a, Default a) => FromJSON (DefaultJSON a) where
parseJSON v = DefaultJSON <$> (parseJSON v <|> pure def)
data AppRefs = AppRefs
{ twsConnectionRefs :: TWSConnectionRefs
, currentAccount :: TVar (Maybe Text)
, appCharts :: TVar (HashMap Text (TVar Chart))
, tickerIdToSymbol :: TVar (HashMap Int Text)
}
data App = App
{ appSettings :: !Settings
, appLogFunc :: !LogFunc
, appProcessContext :: !ProcessContext
, appOptions :: !Options
, appWindow :: !Window
, appRefs :: !AppRefs
, appData :: !DataRefs
-- Add other app-specific configuration information here
}
instance HasLogFunc App where
logFuncL = lens appLogFunc (\x y -> x { appLogFunc = y })
instance HasProcessContext App where
processContextL = lens appProcessContext (\x y -> x { appProcessContext = y })

11
src/Util.hs Normal file
View File

@ -0,0 +1,11 @@
{-# LANGUAGE NoImplicitPrelude #-}
-- | Silly utility module, used to demonstrate how to write a test
-- case.
module Util
( plus2
) where
import RIO
plus2 :: Int -> Int
plus2 = (+ 2)

79
stack.yaml Normal file
View File

@ -0,0 +1,79 @@
# This file was automatically generated by 'stack init'
#
# Some commonly used options have been documented as comments in this file.
# For advanced use and comprehensive documentation of the format, please see:
# https://docs.haskellstack.org/en/stable/yaml_configuration/
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# resolver: lts-3.5
# resolver: nightly-2015-09-21
# resolver: ghc-7.10.2
#
# The location of a snapshot can be provided as a file or url. Stack assumes
# a snapshot provided as a file might change, whereas a url resource does not.
#
# resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-18.24
# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# subdirs:
# - auto-update
# - wai
packages:
- .
- deps/dear-imgui.hs
# Dependency packages to be pulled from upstream that are not in the resolver.
# These entries can reference officially published versions as well as
# forks / in-progress versions pinned to a git hash. For example:
#
# extra-deps:
# - acme-missiles-0.3
# - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#
extra-deps:
- type-iso-1.0.1.0@sha256:75682a06a5af1798c6641ba3cc175685a1f699962ad22ab194a487c0d6b7da66,1892
- numericpeano-0.2.0.0@sha256:e3a1dc960817a81f39d276e7bfa0124e8efa1b91b5c272a70dfa16c38627f172,1406
allow-newer: true
# Override default flag values for local packages and extra-deps
flags:
dear-imgui:
# libraries
glfw: true
sdl: false
vulkan: false
# hardware-requirements
opengl3: true
opengl2: false
# Extra package databases containing global packages
# extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
#
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: ">=2.7"
#
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
#
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
#
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

1
test/Spec.hs Normal file
View File

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

14
test/UtilSpec.hs Normal file
View File

@ -0,0 +1,14 @@
{-# LANGUAGE NoImplicitPrelude #-}
module UtilSpec (spec) where
import Import
import Util
import Test.Hspec
import Test.Hspec.QuickCheck
spec :: Spec
spec = do
describe "plus2" $ do
it "basic check" $ plus2 0 `shouldBe` 2
it "overflow" $ plus2 maxBound `shouldBe` minBound + 1
prop "minus 2" $ \i -> plus2 i - 2 `shouldBe` i