Init
This commit is contained in:
commit
befb1ab1eb
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
*~
|
||||
*.swp
|
||||
tarballs/
|
||||
.stack-work/
|
||||
imgui.ini
|
||||
settings.json
|
||||
*.lock
|
||||
tags
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal 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
0
.hlint.yaml
Normal file
3
ChangeLog.md
Normal file
3
ChangeLog.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Changelog for ibhelper
|
||||
|
||||
## Unreleased changes
|
30
LICENSE
Normal file
30
LICENSE
Normal 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
10
README.md
Normal 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`
|
108
app/Main.hs
Normal file
108
app/Main.hs
Normal 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
1
deps/dear-imgui.hs
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e5969f6b358dab01d9e2bea8d3538fdfbee0c2f1
|
1
deps/dear-implot.hs
vendored
Submodule
1
deps/dear-implot.hs
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 78f7df091ceb88f57eaacdb0a1e2d43cbce04566
|
248
ibhelper.cabal
Normal file
248
ibhelper.cabal
Normal 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
121
package.yaml
Normal 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
79
src/AppFiller.hs
Normal 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
89
src/Chart.hs
Normal 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
|
95
src/IBClient/Connection.hs
Normal file
95
src/IBClient/Connection.hs
Normal 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
581
src/IBClient/Types.hs
Normal 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
16
src/Import.hs
Normal 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
230
src/Run.hs
Normal 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
240
src/Types.hs
Normal 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
11
src/Util.hs
Normal 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
79
stack.yaml
Normal 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
1
test/Spec.hs
Normal file
@ -0,0 +1 @@
|
||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
14
test/UtilSpec.hs
Normal file
14
test/UtilSpec.hs
Normal 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
|
Loading…
Reference in New Issue
Block a user