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 = stock("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
Rough sketch of underlying structure
Pure functions
on single values
f :: a -> b
+1
/2
on multiple values
f :: a -> b -> c
a > b
a - b
on list of values
f :: [a] -> b
sum a
max a
Time Series and Signals
a is Floating
TimeSeries a
a is Bool
or ADT
Signal a
Both TimeSeries
and Signal
should implement Functor
and Applicative
, but not Monad
Functions on single time series (Functor)
fmap :: (a -> b) -> TimeSeries a -> TimeSeries b (Functor)
Stateful fmap i.e. keep a state through fmap operation
smap :: (a -> b -> (b, c)) -> c -> TimeSeries a -> TimeSeries c
Combinations of time series (Applicative)
f (a -> b) -> f a -> f b
combine :: (a -> b -> c) -> [TimeSeries a] -> TimeSeries [b] -> TimeSeries [c]
merge :: [TimeSeries a] -> TimeSeries [a]
Convolution and reordering of time series
delay :: Int -> b -> TimeSeries a -> TimeSeries b
windowed :: Int -> a -> TimeSeries a -> TimeSeries [a]
reordering ???
Combinations of Time series and Signals
f :: (a -> b) -> TimeSeries a -> Signal b
g :: (a -> b) -> Signal a -> TimeSeries b
auto
package as alternative
Evaluate Evaluate auto package.TBD.
Platform idea
Enable a platform where pure scripts with very strictly defined and typed inputs and outputs can be easily uploaded updated and shared between different scripts.
- Compare with AWS lamda functions for defining new processing tools for displaying stock data
- Allow a viewer to show any stock input source with a compatible function
The platform is partly openly available as a default. But subscription with increased access can be bought.
For fast viewing SVG could be processed progressively on the backend and fast scrolling could be enabled on the front end without any server interaction. See e.g. ChartIQ
References
- TradingView - a quite advanced trading platform
- d3.js - javascript diagramming framework
- TechanJS - TA built on d3
- The Essence of Dataflow Programming
- APEX Charts - MIT licensed
- ContEx - Server side rendering in Elixir
- HighCharts - Used by Avanza