Wiki2
FunStock

Functional Stock Analysis (FunStock)

Introduction

The target is to evaluate if a language (domain specific) for stock market analysis can be designed to make it both powerful and simple to use for most users.

Inspiration comes from:

I had a few ideas when I started evaluating this topic:

  • Make the language mathematical/functional rather than imperative
  • Do operations on time series (as a whole) not on individual data points at a specific time
  • Harmonize between indicators (line diagrams) and signals (buy/sell events)

Status

At present it’s a simple proof of concept. A set of data structures and functions has been defined in Haskell which allow simple analysis to be performed.

The following features are supported.

  • A time series datatype has been defined to hold stock data, indicators and signals (TimeSeries.hs)
  • A set of general functions operating on time series has been defined (TimeSeries.hs)
  • A set of example indicators has been defined which is based on basic functionality of time series (StockIndicators.hs)
  • Stock data can be downloaded from google finance (StockIo.hs)
  • Simple diagrams can be generated as SVG (StockView.hs)

There are still a lot more to do to make this useful or complete. For instance.

  • Scripts are plain Haskell code which need to be compiled (not usable by most users)
  • Diagramming features are very limited
  • It’s a command line tool which generates a single SVG file (no UI)

But it has somewhat helped me to further understand how to design a stock analysis language.

An example

An example diagram is generated by FunStock.hs.

It does the following.

  • Load share stock data from a file (stocks/aapl.csv) as time series
  • Calculates a 10 step moving average as an indicator
  • Defines a signal which trigger when MA10 indicator passes below stock indicator
  • Draws a diagram with the two indicators and the signal

The code is shown below.

main =
  do
    s <- loadShareFile "aapl"
    let aapl = select Io.Close s
    let ma10 = movingAvarage 10 aapl
    let cross = crossesBelow ma10 aapl 
    let indicators = [ ("AAPL", opaque blue, aapl)
                     , ("MA10", opaque red, ma10)
                     ]
    let signals = [ ("MA10 cross below", cross) ]
    draw "aapl.svg" indicators signals

The Haskell code is quite straight forward. It could be made even easier to read if a domain specific language were used, which more or less directly could map towards the Haskell implementation.

In such case the syntax could look like the following.

aapl = load("aapl").close
ma10 = movingAvarage(10, aapl)
cross = crossesBelow(ma10, aapl) 
indicators = [ ("AAPL", blue, aapl), ("MA10", red, ma10) ]
signals = [ ("MA10 cross below", cross) ]
draw "aapl.svg" indicators signals

The idea is that it shall be relatively simple to extend with new functions like movingAvarage and crossesBelow.

For instance movingAvarage (in StockIndicators.hs) is quite simple.

mean :: (Floating a) => [a] -> a
mean xs = sum xs / len xs

-- Calculate moving average for a time series over n number of data points
movingAvarage :: (Floating a) => Int -> TimeSeries a -> TimeSeries a
movingAvarage n ts = TS.map mean $ slidingWindow n ts

It makes use of slidingWindow (in TimeSeries.hs) which convert a single valued time series to a multi valued time series of a specified window length.

movingAvarage is done two steps.

First, the time series is fed through the slidingWindow function.

slidingWindow n ts

Then the mean function is “mapped over” the “windowed” time series.

TS.map mean $ ...

It turns out that most simple indicators (i.e. indicators that only depend on “current” value) can be implemented through a simple map function.

-- Map for time series
map :: (a -> b) -> (TimeSeries a) -> (TimeSeries b)
map f ts@(TS _ _ as) = ts { series = P.map f as } where

For instance aapl + 10 would be written as.

aaplPlus10 = TS.map (+10) aapl

Many indicators are however stateful i.e. they are dependent on previous data in the time series.

For instance in slidingWindow the “current” value is a list of the previous N values. This is a typical stateful operation.

-- Make sliding window from time series. Use a fifo as state
slidingWindow :: Int -> TimeSeries a -> TimeSeries [a]
slidingWindow n ts = smap window s0 ts where
  s0 = F.mkEmpty n
  window s e = let s' = push s e in (s', F.toList s')

To do stateful map operation there is a special smap (in TimeSeries.hs) which keeps a custom state through the map.

-- Stateful map functions for time series. It runs through series from chronologically, while
-- keeping a state through the process. Map function :: state -> elem -> (state', elem')
smap :: (s -> a -> (s, b)) -> s -> TimeSeries a -> TimeSeries b
smap f s0 ts@(TS _ _ as) = ts { series = as' } where
  as' = reverse $ snd $ foldl smap' (s0, []) as
  smap' (s, es') e = let (s', e') = f s e in (s', e':es')

smap is similar to map but it also permit a state to “travel” through the map.

To implement slidingWindow we make use of a FIFO queue, to keep a list of the most N recent values of the input, as the state in the stateful map operation.

crossesBelow (in StockIndicators.hs) use a different strategy.

-- Determine when time series a cross from below of b from previous value to current
crossesBelow :: (Num a, Ord a) => TimeSeries a -> TimeSeries a -> Signal
crossesBelow a b = mkSignal id s where
  a' = combine (,) (TS.delay 1 a) a
  b' = combine (,) (TS.delay 1 b) b
  s = combine crossBelow a' b'
  -- Does (a_prev,a) pass (b_prev,b) from below i.e. is a_prev < b_prev && a > b ?
  crossBelow :: (Num a, Ord a) => (a,a) -> (a,a) -> Bool
  crossBelow (a',a) (b',b) | a' < b' && a > b = True
                           | otherwise        = False

To determine if one time series passes another time series at a specified time instant (t) you need to look at the current value as well as the previous value. You could do this by keeping the previous value as state in a stateful map operation, but here we use a different strategy.

Instead, we take each time series and “combines” it with a delayed (one time step) version of itself. combine (in TimeSeries.hs) takes two time series and combines its values by a supplied function. In this case we make a “tuple” from the two time series which creates a new time series with both current and previous value (a_prev,a) as its tuple values.

crossBelow is a pure function that takes two tuples (from series a and b) at current and previous time ((a_prev,a) and (b_prev,b)) to determine if a passed b from previous time step.

Finally crossBelow is combined with the “tupled” time series to create a time series with Bool values. mkSignal convert a time series to a signal using a conversion function. The conversion function is in this case the identity function id as the time series alread have the final Bool values.

The resulting SVG diagram is shown below.

The complete code

The whole code for the proof of concept can be found here.

funstock.tgz - All source code

Evaluate auto package as alternative

Evaluate auto package.TBD.

References