packages feed

simfin-1.0.0: examples/EarningsPerShare.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main
  ( main
  ) where

import Control.Arrow ((&&&))
import Control.Monad
import Data.Time.Clock
import Data.Time.Calendar
import Data.Time.LocalTime
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Multimap as MM
import Data.Either
import qualified Data.Text as T
import Graphics.Rendering.Chart.Easy hiding (close)
import Graphics.Rendering.Chart.Backend.Diagrams

import SimFin.Free
import qualified SimFin.Types.Derived as D

type LinePt = (LocalTime, Double)
type LineData = (String, NonEmpty LinePt)

to5pm :: Day -> LocalTime
to5pm = flip LocalTime $ dayFractionToTimeOfDay $ 17 / 24

toPoint :: DerivedRow Double -> Maybe LinePt
toPoint row = do
  eps <- earningsPerShareDiluted row
  let day = reportDate (row :: DerivedRow Double)
  pure (to5pm day, eps)

stocks :: [StockRef]
stocks = ["GOOG", "AAPL", "TWTR", "NFLX"]

-- Caution! This can blow through API limits.
stockQueries :: Integer -> [StatementQueryFree]
stockQueries year = do
  stock <- stocks
  year <- [2016..fromInteger year]
  period <- [Q1, Q2, Q3, Q4]
  pure $ StatementQueryFree
    { stockRef = stock
    , period = period
    , year = year
    , ttm = False
    }

groupOn' :: Ord b => (a -> b) -> [a] -> [(b, NonEmpty a)]
groupOn' f lst = M.toList $ MM.toMap $ MM.fromList $ (f &&& id) <$> lst

toLines :: [DerivedRow Double] -> [LineData]
toLines rows = do
  (ticker, rows') <- groupOn' D.ticker rows
  pts <- maybeToList $ traverse toPoint rows'
  pure (T.unpack ticker, pts)

main :: IO ()
main = do
  now <- getCurrentTime
  let (year, _, _) = toGregorian $ utctDay now
  ctx <- createDefaultContext

  pricesRes :: [ApiResult (Maybe (DerivedRow Double))] <- traverse (fetchDerived ctx) $ stockQueries year
  let prices :: [LineData] = toLines $ catMaybes $ rights pricesRes

  toFile def "eps-diluted.svg" $ do
    layout_title .= "Diluted Earnings per Share"
    layout_background .= solidFillStyle (opaque white)
    layout_foreground .= opaque black
    layout_left_axis_visibility . axis_show_ticks .= False
    layout_x_axis . laxis_title .= "Date"
    layout_y_axis . laxis_title .= "Diluted EPS ($)"
    forM_ prices $ \(name, pts) -> plot $ line name [NE.toList pts]
  pure ()