I work in the business of algorithmic power trading, which is the automated trading of various power-related products in regulated electricity markets. Products include short-term inter-jurisdiction arbitrage, financial transmission rights, and more.
This year, my employer is expanding its trading operations to a new class of products. Since there is no overlap between this new work and our current operations, I got to design a technology stack most suited for the task. This technology stack includes Haskell, most importantly because wrong or unexpected trading decisions can (and have) cost us dearly.
In this blog post, I want to show you the basics of how we designed the framework in which to express trading strategies.
The fundamentals of trading strategies
The fundamental pieces of trading operations are strategies. In algorithmic trading, strategies are computer programs that decide what to trade, and how to trade it, at any given moment.
Let’s take the example of a simple trading strategy that is only concerned with AAPL stock. The current stock price is about 190 USD today; our example strategy is defined thusly:
- If the AAPL price rises above 200, sell our holdings (if we have any);
- If the AAPL price falls below 180, buy 10 shares;
In this case, the result of this strategy is some signal to buy or sell AAPL stock. We can run our strategy in a loop:
import qualified Control.Monad
data Action = Buy Int
| Sell
| Hold
type Price = Double
main :: IO ()
= Control.Monad.forever $ do
main <- fetchMostRecentPrice
aaplPrice let action = myStrategy aaplPrice
executeMarketAction actionwhere
myStrategy :: Price -> Action
myStrategy aapl_price| aapl_price > 200 = Sell
| aapl_price < 180 = Buy 10
| otherwise = Hold
fetchMostRecentPrice :: IO Price
= (...)
fetchMostRecentPrice
executeMarketAction :: Action -> IO ()
= (...) execureMarketAction
and boom, you have a simple trading system!
Once you have a good idea for a strategy, you should test it on historical data. This is called backtesting. Backtesting strategies is, by definition, much more computationally intensive than live trading, since you are evaluating your strategy on much more data. We often backtest strategies on 5-10 years’ worth of data when it makes sense, and sometimes more.
I will also note that it is easiest to have strategies that can run in various contexts (including backtesting and live operations) if the strategy is pure (in the mathematical sense). It is in the quest for purity and performance that we decided to implement the trading system for a new asset class in Haskell.
Trading strategies in Haskell
For the simplicity of presentation, we will only consider strategies that involve prices. The simplest such strategies are strategies which depend on the most recent price:
newtype Strategy
= MkStrategy { runStrategy :: Price -> Action }
Strategy
is a type of functions, from the most recent Price
known to some market action. This is only re-packaging the example above.
Let’s build a backtesting framework. There are two parts here:
- determine historical market actions;
- simulate the effects of market actions.
In practice, the two parts of backtesting are handled simultaneously. However, for simplicity, I will only consider the first part here.
The nature of this problem is well-suited to streaming approaches; I will use pipes
1:
-- From the `time` package
import Data.Time ( UTCTime )
-- From the `pipes` package
import Pipes ( Producer, (>->) )
import qualified Pipes
import qualified Pipes.Prelude as Pipes
-- | From a stream of input features, produce a stream
-- of output 'Market' actions
backtestStrategy :: Monad m
=> Strategy r
-> Producer (UTCTime, Price) m () -- ^ stream of timestamped AAPL prices
-> m BacktestResults
backtestStrategy strat prices = prices
>-> Pipes.map (\(k, f) -> (k, runStrategy strat f))
>-> simulateMarketActions
-- The following is out-of-scope
data BacktestResults = MkBacktestResults (...)
simulateMarketActions :: Consumer (UTCTime, Action) m BacktestResults
= (...) simulateMarketActions
More expressive strategies
I have a problem with the above definition of Strategy
: I’m limited to strategies based on the single, most recent Price
. What if I had a good idea for a strategy which involves the last 10 price values? Our Strategy
type is not expressive enough: it only takes one type of feature, while we want to support a wide range of features.
I will define a Strategy
type which removes restrictions on the input feature:
newtype Strategy r
= MkStrategy { runStrategy :: r -> Action }
with the understanding that the data of type r
is somehow derived from prices.
What are some features derived from prices, that we might be interested in?
- Price history, e.g. most recent N ticks;
- Price aggregations, e.g. average of most recent M ticks;
- Rolling aggregations, e.g. N-tick history of the averages of M ticks;
Every conceptual feature described above has some free parameters. We don’t want to have separate strategies like Strategy PriceHistoryForPast10Ticks
and Strategy PriceHistoryForPast20Ticks
. More specifically, for every feature of type r
, there is a type of parameters p
which describes the parameters of r
.
For example2:
-- from the `javelin` pacakge
import Data.Series ( Series )
newtype PriceHistory
= MkPriceHistory (Series UTCTime Price)
data NumTicks
= MkNumTicks { numTicks :: Int }
Typed features and their parametrization
We could list all possible features in a big sum type:
data Feature
= FPrice Price
| FPriceHistory (Series UTCTime Price)
| FAveragePrice Price
| (...)
However, it’s not possible to control what features go in what strategy. We can do better.
We want to be able to link the types PriceHistory
and NumTicks
such that they are used together when backtesting, to ensure type safety. This is the domain of indexed type families, or type families for short. We will amend our Feature
typeclass and backtestStrategy
function to take into account feature parametrization:
class Feature r where
-- For every instances `r` of `Feature`,
-- there is an associated type `Parameters r` which the user
-- needs to specify. See examples below.
type Parameters r
deriveFeature :: Monad m
=> Parameters r
-> Producer (UTCTime, Price) m ()
-> Producer (UTCTime, r) m ()
backtestStrategy :: (Feature r, Monad m)
=> Strategy r
-> Parameters r
-> Producer (UTCTime, Price) m ()
-> m BacktestResults
backtestStrategy strat params prices = deriveFeature params prices
>-> Pipes.map (\(k, feature) -> (k, runStrategy strat feature))
>-> simulateMarketActions
Let’s look at two example instances of Feature
. The simplest is the basic feature or Price
:
type NoParameters = ()
instance Feature Price where
-- The `Price` feature has no free parameters
type Parameters Price = NoParameters
deriveFeature :: Monad m
=> NoParameters
-> Producer (UTCTime, Price) m ()
-> Producer (UTCTime, Price) m ()
= prices deriveFeatures _ prices
This is easy because there are no parameters. What about looking at the price history?
newtype PriceHistory
= MkPriceHistory (Series UTCTime Price)
newtype NumTicks
= MkNumTicks { numTicks :: Int }
instance Feature PriceHistory where
type Parameters PriceHistory = NumTicks
deriveFeature :: Monad m
=> NumTicks
-> Producer (UTCTime, Price) m ()
-> Producer (UTCTime, PriceHistory) m ()
MkPriceHistoryParameters numTicks) prices
deriveFeature (= prices >-> accumulate numTicks
>-> Pipes.map (\xs -> (maximum $ Series.index xs, MkPriceHistory xs))
where
-- out of scope, see end of blog post for link to source
accumulate :: Functor m
=> Int
-> Pipe (UTCTime, a) (Series UTCTime a) m ()
= (...) accumulate
Finally, as an example of the power of this approach, we’ll create a strategy which combines two features.
First, we’ll extend the Feature
class to combine two features a
and b
into one (a, b)
feature:
instance (Feature a, Feature b) => Feature (a, b) where
type Parameters (a, b) = (Parameters a, Parameters b)
deriveFeature :: Monad m
=> Parameters (a, b)
-> Producer (UTCTime, Price) m ()
-> Producer (UTCTime, (a, b)) m ()
deriveFeature (paramsA, paramsB) prices = Pipes.zipWith (\(k,a) (_, b) -> (k, (a, b)))
(deriveFeature paramsA prices) (deriveFeature paramsB prices)
Second, we’ll define a simple strategy that compares the most recent price against the average price of the last 10 ticks:
import Data.Series ( fold, mean )
finalStrategy :: Strategy (PriceHistory, Price)
finalStrategy = MkStrategy $ \(MkPriceHistory history, price)
-> let avgPrice = fold mean history
in case price `compare` avgPrice of
GT -> Sell
LT -> Buy 10
EQ -> Hold
It is trivial to backtest this strategy like so:
backtestFinalStrategy :: Monad m
=> Producer (UTCTime, Price) m ()
-> m BacktestResults
= backtestStrategy finalStrategy ( MkNumTicks 10, () ) backtestFinalStrategy
and voilà!
Conclusion
In this post, I have shown you how to define trading strategies with typed feature parametrization, which is a neat use of type families.
All code is available in this Haskell module.
If you are unfamiliar with
pipes
, you should check out its tutorial.↩︎We are storing the price history in a
Series
, which comes from thejavelin
package that I created specifically for this work.↩︎