I am always amused when retail traders are offered and then do use black boxes with complex frameworks to build automatic trading strategies. Usually, that goes with an explanation how cool those platforms are and how easy to build a strategy using them. They forget about vendor lock-in and the most terrible model of shared state with events hell like “OnData”, “OnOrder”, “OnBarOpen”, “OnBarClose”, etc. The more I talk with people, the more I realize that CEP is the approach that desks and serious traders use.

Spreads is not a framework for financial markets but a generic CEP library that allows to model markets as data series and use pure math and functional data transformations.

The simplest trivial example is simple moving average trend following strategy. This example is written in F# interactive console.  First, we generate trending artificial data where trend changes each 40 points:

 1let quotes : Series<DateTime, float> = // data is produced outside
 2let mutable previous = 1.0
 3let sm = SortedMap()
 4let now = DateTime.UtcNow
 5let mutable trend = -1.0
 6let mutable cnt = 0
 7for i in 0..500 do
 8  previous <- previous*(1.0 + rng.NextDouble()*0.002 - 0.001 + 0.001 * trend)
 9  sm.Add(now.AddSeconds(-((500-i) |> float)*0.2), previous)
10  cnt <- cnt + 1
11  if cnt % 40 = 0 then trend <- -trend
12
13Task.Run((fun _ ->
14    while not ct.IsCancellationRequested do
15      Thread.Sleep(500)
16      previous <- previous*(1.0 + rng.NextDouble()*0.002 - 0.001 + 0.001 * trend)
17      sm.Add(DateTime.UtcNow, previous)
18      cnt <- cnt + 1
19      if cnt % 40 = 0 then trend <- -trend
20  ), ct) |> ignore
21sm :> Series<DateTime, float>

Then we calculate simple moving average over 20 points. The second argument allows using incomplete windows, i.e. calculating the average over the first 1, 2, … 19 data points. With false, the first SMA point is calculated only at the 20th quotes point.

1let sma = quotes.SMA(20, true)

Our trading rule is that if the current price is above SMA, we go long, and we go short otherwise. This is a classic trend-following strategy and it works quite well in the long run on emerging markets and some commodities. We calculate our target position as:

1let targetPosition = (quotes / sma - 1.0).Map(fun deviation -> double <| Math.Sign(deviation))

This target position series is live - its values are updated in real-time together with quotes. To execute our strategy, we must prepare storage for actual positions and trades:

1// we must keep track of actual position
2let actualPositionWritable = SortedMap<DateTime,float>()
3let realTrades = SortedMap<DateTime,float>() 
4let actualPosition = actualPositionWritable :> Series<_,_>
5actualPosition.Do((fun k v -> 
6    Console.WriteLine("Actual position: " + k.ToString() + " : " + v.ToString())
7  ), ct)

Then we could feed our target position to a trader. The trader is “functional”, instead of receiving orders it receives the desired state and does its best to move actual state to the desired state.  Here for simplicity, we have a very dangerous assumption that trades are executed  immediately after a signal. Such an assumption should be avoided in real world backtesting. We use the Do() extension method that invokes an action over each key/value in a series sequentially:

 1targetPosition.Do(
 2  (fun k v ->
 3    if k <= DateTime.UtcNow.AddMilliseconds(-400.0) then
 4      // simulate historical trading
 5      let qty = 
 6        if actualPositionWritable.IsEmpty then v
 7        else (v - actualPositionWritable.Last.Value)
 8      if qty <> 0.0 then
 9        Console.WriteLine(k.ToString() +  " : Paper trade: " + qty.ToString())
10        actualPositionWritable.AddLast(k, v)
11    else
12      // do real trading
13      let qty =
14        if actualPositionWritable.IsEmpty then failwith "must test strategy before real trading"
15        else (v - actualPositionWritable.Last.Value)
16      if qty <> 0.0 && k > actualPositionWritable.Last.Key then // protect from executing history
17        let tradeTime = DateTime.UtcNow.AddMilliseconds(5.0)
18        Console.WriteLine(tradeTime.ToString() +  " : Real trade: " + qty.ToString())
19        realTrades.AddLast(tradeTime, qty)
20        actualPositionWritable.AddLast(tradeTime, v)
21  ), ct)

Now the trading is started and we could see our trades in the FSI console every several seconds.

Finally, we calculate our P&L. Because the market was trending by construction, we have earned a lot of money with this strategy:

1let returns = quotes.ZipLag(1u, fun c p -> c/p - 1.0)
2let myReturns = actualPosition.Repeat() * returns
3let myAumIndex = myReturns.Scan(1.0, fun st k v -> st*(1.0 + v))

Here, we use ZipLag() to calculate price returns. Then we calculate returns of our position and aggregate them as running product (no trading costs in this example). All these and above series are live, we could use the Do() method to print the values in real-time:

1// Print live data
2myAumIndex.Do((fun k v -> 
3    Console.WriteLine("AUM Index: " + k.ToString() + " : " + v.ToString())
4  ), ct)

Note that from the technical point of view there is nothing special that links the calculations to financial markets. Instead of financial data, we could use data from some sensors, e.g. a temperature, and instead of trading, we could turn off/on heating if the temperature goes above or below some target value.

The whole example is here. You could select all code, press Alt+Enter and watch how AUM increases while using Spreads library! :)