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:

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

let 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:

let 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:

// we must keep track of actual position
let actualPositionWritable = SortedMap<DateTime,float>()
let realTrades = SortedMap<DateTime,float>() 
let actualPosition = actualPositionWritable :> Series<_,_>
actualPosition.Do((fun k v -> 
    Console.WriteLine("Actual position: " + k.ToString() + " : " + v.ToString())
  ), 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:

 1 targetPosition.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:

let returns = quotes.ZipLag(1u, fun c p -> c/p - 1.0)
let myReturns = actualPosition.Repeat() * returns
let 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:

// Print live data
myAumIndex.Do((fun k v -> 
    Console.WriteLine("AUM Index: " + k.ToString() + " : " + v.ToString())
  ), 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! :)