implemented #5

This commit is contained in:
Nicole Dresselhaus 2022-07-21 00:43:16 +02:00
parent 1848f0f2fc
commit 971f9d1651
8 changed files with 116 additions and 58 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ settings.json
*.lock *.lock
tags tags
dist-newstyle/ dist-newstyle/
cache/*

2
deps/dear-implot.hs vendored

@ -1 +1 @@
Subproject commit ad5fdc1e646947c1365e725d29a43fc96dc98a66 Subproject commit 49539fc1ba04c590af0718066ce7a9aead8ff336

View File

@ -99,6 +99,7 @@ library
, gl , gl
, managed , managed
, microlens-th , microlens-th
, extra
, network , network
, pretty-show , pretty-show
, rio >=0.1.12.0 , rio >=0.1.12.0

View File

@ -11,7 +11,7 @@ import Data.Time
import Data.FingerTree import Data.FingerTree
import Data.HashMap.Strict ((!?)) import Data.HashMap.Strict ((!?))
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.List as L
import qualified Debug.Trace as D import qualified Debug.Trace as D
@ -28,11 +28,14 @@ appFiller app = runRIO app $ withRunInIO $ \run -> do
(Msg_IB_IN (IB_ManagedAccts as)) -> do (Msg_IB_IN (IB_ManagedAccts as)) -> do
cur <- readTVarIO $ Types.accounts currentAppData cur <- readTVarIO $ Types.accounts currentAppData
actions <- forM as $ \a -> case cur !? a of actions <- forM as $ \a -> case cur !? a of
Just _ -> return $ id Just _ -> return id
Nothing -> do Nothing -> do
debugMsg $ "added Account "<> a debugMsg $ "added Account "<> a
return $ HM.insertWith const a (mkIBAccount a) return $ HM.insertWith const a (mkIBAccount a)
atomically $ modifyTVar (Types.accounts currentAppData) $ foldl' (.) id actions atomically $ modifyTVar (Types.accounts currentAppData) $ foldl' (.) id actions
run $ whenM (ask >>= (fmap isNothing . readTVarIO) . currentAccount . appRefs)
$ unless (L.null as)
$ switchAccountTo $ L.head as
(Msg_IB_IN (IB_NextValidID i)) -> atomically $ modifyTVar' (nextValidID currentAppData) (const (Just i)) (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_ErrorMsg i c m)) -> debugMsg "IB_ErrorMsg not implemented"
(Msg_IB_IN (IB_AccountValue k v c n)) -> do (Msg_IB_IN (IB_AccountValue k v c n)) -> do
@ -58,7 +61,6 @@ appFiller app = runRIO app $ withRunInIO $ \run -> do
_ -> --D.trace ("not implemented in AppFiller:" <> show input) $ _ -> --D.trace ("not implemented in AppFiller:" <> show input) $
infoMsg $ "not implemented in AppFiller:" <> T.pack (show input) infoMsg $ "not implemented in AppFiller:" <> T.pack (show input)
handleTickPrice :: IB_IN -> RIO App () handleTickPrice :: IB_IN -> RIO App ()
handleTickPrice IB_TickPrice{..} = do handleTickPrice IB_TickPrice{..} = do
charts <- appCharts . appRefs <$> ask charts <- appCharts . appRefs <$> ask
@ -71,7 +73,7 @@ handleTickPrice IB_TickPrice{..} = do
case tickType of case tickType of
IBTickType_Last_Price -> do IBTickType_Last_Price -> do
t <- utctDayTime <$> liftIO getCurrentTime t <- utctDayTime <$> liftIO getCurrentTime
let cp = ChartPoint (TimePoint $ fromInteger. (`div` 1000000000000) . diffTimeToPicoseconds $ t) price [] let cp = ChartPoint (TimePoint $ fromInteger. (`div` 1000000000000) . diffTimeToPicoseconds $ t) price size []
liftIO $ atomically $ modifyTVar chartVar (\c@Chart{..} -> c { chartData = chartData |> cp, chartDirty = True}) liftIO $ atomically $ modifyTVar chartVar (\c@Chart{..} -> c { chartData = chartData |> cp, chartDirty = True})
_ -> return () _ -> return ()
handleTickPrice _ = error "impossible" handleTickPrice _ = error "impossible"

View File

@ -2,18 +2,21 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DerivingVia #-}
module Chart (newChart, FillerException(..)) where module Chart (newChart, FillerException(..), getUpdatedChartCache) where
import Import import Import
import Data.Time import Data.Aeson (eitherDecodeFileStrict')
import RIO.List import RIO.List
import RIO.List.Partial import RIO.List.Partial
import Data.Time.Calendar (Day(..))
import Data.FingerTree (FingerTree) import Data.FingerTree (FingerTree)
import Control.Concurrent (forkIO) import Control.Concurrent (forkIO)
import Control.Monad.Extra (ifM)
import qualified RIO.ByteString as BS import qualified RIO.ByteString as BS
-- import Control.Exception -- import Control.Exception
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.FingerTree as FT import qualified Data.FingerTree as FT
import qualified Data.Text as T
import qualified Debug.Trace as D import qualified Debug.Trace as D
@ -28,10 +31,20 @@ newChart :: IBContract -> RIO App ()
newChart contract = do newChart contract = do
app <- ask app <- ask
let sym = (symbol :: IBContract -> Text) contract let sym = (symbol :: IBContract -> Text) contract
con = (conId :: IBContract -> Int) contract
hmVar = appCharts . appRefs $ app hmVar = appCharts . appRefs $ app
hm <- liftIO . readTVarIO $ hmVar hm <- liftIO . readTVarIO $ hmVar
unless (sym `HM.member` hm) $ do unless (sym `HM.member` hm) $ do
c <- liftIO $ newTVarIO $ Chart FT.empty mempty undefined defChartSettings [] Nothing False let cacheFileName = "cache/"<> show con <>".json"
today <- liftIO getCurrentDay
cacheData <- liftIO $ ifM (doesFileExist cacheFileName)
(eitherDecodeFileStrict' cacheFileName)
(return $ Left $ "cachefile "<>cacheFileName<>" not found.")
(cacheData' :: HashMap Day (FingerTree TimePoint ChartPoint)) <- case cacheData of
Left err -> logError (display (T.pack $ "Error opening "<>cacheFileName<>".\n") <> displayShow err)
>> return HM.empty
Right (d :: HashMap Day [ChartPoint]) -> return $ FT.fromList <$> d
c <- liftIO $ newTVarIO $ Chart con (fromMaybe FT.empty $ cacheData' HM.!? today) mempty undefined defChartSettings [] Nothing False
tid <- liftIO $ forkIO $ fillChart app contract c tid <- liftIO $ forkIO $ fillChart app contract c
liftIO $ atomically $ do liftIO $ atomically $ do
modifyTVar' c (\Chart{..} -> let fillerThread = tid in Chart{..}) modifyTVar' c (\Chart{..} -> let fillerThread = tid in Chart{..})
@ -54,36 +67,42 @@ fillChart app contract cVar = runRIO app $ do
handle (\QuitFiller -> cancelSubscription >> exitSuccess) $ handle (\QuitFiller -> cancelSubscription >> exitSuccess) $
forever $ do forever $ do
-- chart dirty? set clean & begin work -- chart dirty? set clean & begin work
Chart{..} <- liftIO (readTVarIO cVar) c <- liftIO (readTVarIO cVar)
when chartDirty $ do when (chartDirty c) $ do
liftIO $ atomically $ modifyTVar cVar (\c -> c { chartDirty = False }) liftIO $ atomically $ modifyTVar cVar (\c -> c { chartDirty = False })
let (TimePoint cacheUpdateStart) = fromMaybe (TimePoint 0) Nothing -- TODO: lastCacheUpdate let (lUpdate, cachePoints) = getUpdatedChartCache c Nothing
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 }) liftIO $ atomically $ modifyTVar cVar (\c -> c { chartCache = cachePoints, lastCacheUpdate = lUpdate })
return () return ()
threadDelay 1000000 -- sleep 5 seconds threadDelay 1000000 -- sleep 5 seconds
getUpdatedChartCache :: Chart -> Maybe ChartSettings -> (Maybe TimePoint, [ChartPoint])
getUpdatedChartCache Chart{..} chartSettings' = (lUpdate, takeWhile (\ChartPoint{..} -> (\(TimePoint x) -> x < cacheUpdateStart) timeOfDay) chartCache <> map toCachePoint chunkedChart)
where (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 $ fromMaybe chartSettings chartSettings') chartData
lUpdate = fmap fst . lastMaybe $ chunkedChart
toCachePoint :: (TimePoint,[ChartPoint]) -> ChartPoint
toCachePoint (t,[]) = ChartPoint t (-1) 0 []
toCachePoint (t,as) = ChartPoint t m vol [OLHC o l h c]
where
vol = sum $ volume <$> as
as' = pointValue <$> as
ms' = sum $ (\x -> pointValue x * fromIntegral (volume x)) <$> as
m = ms' / fromIntegral vol
o = head as'
c = last as'
l = minimum as'
h = maximum as'

View File

@ -6,7 +6,10 @@ module Import
, module Data.Default , module Data.Default
, module Text.Show.Pretty , module Text.Show.Pretty
, module IBClient.Types , module IBClient.Types
, module System.Directory
, ppShow' , ppShow'
, getCurrentDay
, switchAccountTo
) where ) where
import RIO import RIO
@ -15,10 +18,29 @@ import Data.Aeson (FromJSON, ToJSON)
import Data.Default import Data.Default
import Text.Show.Pretty import Text.Show.Pretty
import IBClient.Types import IBClient.Types
import System.Directory
--- imports not reexported --- imports not reexported
import Data.Text as T import Data.Text as T
import Data.Time.Clock
import Data.Time.Calendar
ppShow' :: Show a => a -> Text ppShow' :: Show a => a -> Text
ppShow' = T.pack . ppShow ppShow' = T.pack . ppShow
getCurrentDay :: IO Day
getCurrentDay = utctDay <$> getCurrentTime
switchAccountTo :: Text -> RIO App ()
switchAccountTo a = do
refs' <- appRefs <$> ask
let sendQ = twsConnectionSend $ twsConnectionRefs refs'
-- 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)

View File

@ -14,14 +14,17 @@ import DearImGui
import DearImGui.Plot import DearImGui.Plot
import DearImGui.OpenGL3 import DearImGui.OpenGL3
import DearImGui.SDL import DearImGui.SDL
import Data.Time.Clock
import Graphics.GL import Graphics.GL
import SDL import SDL
import Data.StateVar --import Data.StateVar
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.List as L
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.FingerTree as FT --import qualified Data.FingerTree as FT
import IBClient.Connection import IBClient.Connection
import Import (Chart(chartContractID))
run :: RIO App () run :: RIO App ()
run = do run = do
@ -47,7 +50,18 @@ shutdownApp = do
liftIO $ encodeFile "settings.json" settings' liftIO $ encodeFile "settings.json" settings'
logInfo $ display ("Settings Saved" :: Text) logInfo $ display ("Settings Saved" :: Text)
-- save cached data -- save cached data
liftIO $ unlessM (doesDirectoryExist "cache") $ createDirectory "cache"
charts <- liftIO . readTVarIO . appCharts $ refs
forM_ (HM.toList charts) $ \(symbol,tc) -> do
c@Chart{..} <- liftIO . readTVarIO $ tc
today <- liftIO $ utctDay <$> getCurrentTime
let (_, chartData') = getUpdatedChartCache c (Just $ ChartSettings 5 Nothing Nothing)
merge a b = a <> b --TODO: merge duplicates!
newData = HM.alter (\case
Just t -> Just (merge t chartData')
Nothing -> Just chartData') today (toList <$> chartHistData)
newData' = filter (\ChartPoint{..} -> 0 /= volume) <$> newData
liftIO $ encodeFile ("cache/"<> show chartContractID <>".json") newData'
logInfo $ display $ ppShow' settings' logInfo $ display $ ppShow' settings'
renderLoop :: RIO App () renderLoop :: RIO App ()
@ -91,15 +105,7 @@ renderLoop = do
forM_ accs $ \a -> do forM_ accs $ \a -> do
selectable a >>= \case selectable a >>= \case
False -> return () False -> return ()
True -> do True -> switchAccountTo a
-- 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 let cStatus = twsConnectionStatus cr
connHost <- liftIO $ readTVarIO $ twsConnectionRefsHost cr connHost <- liftIO $ readTVarIO $ twsConnectionRefsHost cr
connPort <- liftIO $ readTVarIO $ twsConnectionRefsPort cr connPort <- liftIO $ readTVarIO $ twsConnectionRefsPort cr
@ -222,8 +228,11 @@ renderLoop = do
case viewr chartData of case viewr chartData of
EmptyR -> text "no last price" EmptyR -> text "no last price"
(_ :> ChartPoint{..}) -> text $ fromString $ "Last: " <> show pointValue <> "\nTime: " <> show timeOfDay (_ :> ChartPoint{..}) -> text $ fromString $ "Last: " <> show pointValue <> "\nTime: " <> show timeOfDay
text $ ppShow' chartCache withPlot "Test" $ do
text $ ppShow' lastCacheUpdate -- TODO: set axes
let (x,y) = L.unzip $ (\ChartPoint{..} -> (fromIntegral timeOfDay,pointValue)) <$> chartCache
plotLine (T.unpack symbol) x y
return ()
return () return ()

View File

@ -169,30 +169,34 @@ defChartSettings = ChartSettings 60 Nothing Nothing
-- instance Monoid TimeWindow where -- instance Monoid TimeWindow where
-- mempty = TimeWindow 0 86400 -- mempty = TimeWindow 0 86400
-- TODO: TimePointFloat? or only 1 entry per second?
newtype TimePoint = TimePoint Int newtype TimePoint = TimePoint Int
deriving Eq deriving (Eq, Generic)
deriving newtype Show deriving newtype (Show, Enum, Real, Ord, Num, Integral, FromJSON, ToJSON)
deriving (Semigroup, Monoid) via (Max Int) deriving (Semigroup, Monoid) via (Max Int)
data ChartStudies = SMA { window :: Int, value :: Float } data ChartStudies = SMA { window :: Int, value :: Float }
| OLHC { olhc_open :: Float, olhc_low :: Float, olhc_high :: Float, olhc_close :: Float} | OLHC { olhc_open :: Float, olhc_low :: Float, olhc_high :: Float, olhc_close :: Float}
deriving (Show, Eq) deriving (Show, Eq, Generic, FromJSON, ToJSON)
data ChartPoint = ChartPoint data ChartPoint = ChartPoint
{ timeOfDay :: TimePoint { timeOfDay :: TimePoint
, pointValue :: Float , pointValue :: Float
, volume :: Int
, pointExtra :: [ChartStudies] , pointExtra :: [ChartStudies]
} deriving (Show, Eq) } deriving (Show, Eq, Generic, FromJSON, ToJSON)
instance Measured TimePoint ChartPoint where instance Measured TimePoint ChartPoint where
measure = timeOfDay measure = timeOfDay
data Chart = Chart data Chart = Chart
{ chartData :: FingerTree TimePoint ChartPoint { chartContractID :: Int
, chartHistData :: HashMap Day (FingerTree TimePoint ChartPoint) , chartData :: FingerTree TimePoint ChartPoint -- ^ raw data (time & sale)
, chartHistData :: HashMap Day (FingerTree TimePoint ChartPoint) -- ^ raw data (time & sale)
, fillerThread :: ThreadId , fillerThread :: ThreadId
, chartSettings :: ChartSettings , chartSettings :: ChartSettings
, chartCache :: [ChartPoint] , chartCache :: [ChartPoint] -- ^ cache for drawing with all pointExtra filled out and sampled according to 'chartSettings'
, lastCacheUpdate :: Maybe TimePoint , lastCacheUpdate :: Maybe TimePoint
, chartDirty :: Bool , chartDirty :: Bool
} deriving (Show, Eq) } deriving (Show, Eq)