Strategies
Let’s revisit the simple moving average system from the Introduction to understand a few key concepts.
using Balsam;
class SimpleSma : Strategy{ protected override void OnStrategyStart() { Col1 = Sma(Close, 50); Col2 = Sma(Close, 200); }
protected override void OnBarClose() { if (Col1.Last > Col2.Last) { Buy(); } else { Sell(); } }}Strategy code discussion
Section titled “Strategy code discussion”-
On the first line we import the namespace
Balsam. This is the primary namespace containing key classes for creating and working with strategies. Other important namespaces includeBalsam.DataServersandBalsam.MoneyManagement. -
Next we create a class called
SimpleSmawhich inherits from Strategy, the abstract base class containing core backtesting functionality. All strategies must inherit fromStrategyor a derivative of it. -
Within the new
SimpleSmaclass, we override the virtual methodOnStrategyStart(). This is where strategy initialization is typically performed. Here we calculate two simple moving averages on lines 7 and 8 and assign them to the built-inTimeSeriesvariablesCol1andCol2. Col is short for column. Think of it just like a spreadsheet column. For convenience, there are 20 predefined column variables. You can also declare your ownTimeSeriesvariables within the class if you prefer and it will work just the same. -
Finally, we override the
OnBarClose()method. Like the name suggests, this is called after the close of every new bar and is typically where trading logic is implemented. In spreadsheet terms, this method is called sequentially once for each trading period. Here we reference the moving average values that were assigned to theCol1andCol2variables inOnStrategyStart().Col1.Lastis an alias forCol1[0]. UsingLastcan make code a little easier to understand at a glance. But you might be wondering, ifLastis equivalent to[0], aren’t we always referring to the same observation? If we were working with simple arrays the answer would be yes; however,TimeSerieshave some additional functionality under the hood which brings up a key point you must understand when working with strategies.
Concurrency
Section titled “Concurrency”The backtester automatically maintains concurrency, or the notion that all data is aligned by date. In OnStrategyStart(), TimeSeries act like simple arrays which can be indexed from zero to count - 1. But once the simulation starts running, this behavior changes. Indexing into a TimeSeries within the OnBarClose() method using .Last or [0] works relative to the current date that is being tested.
Here’s a concrete example of this concept in action:
class ConcurrencyExample : Strategy{ protected override void OnStrategyStart() { //create some dummy data Col1 = new TimeSeries() { { new DateTime(2024, 1, 1), 1 }, { new DateTime(2024, 1, 2), 2 }, { new DateTime(2024, 1, 3), 3 }, { new DateTime(2024, 1, 4), 4 }, { new DateTime(2025, 1, 5), 5 } };
Console.WriteLine($"Indexing in OnStrategyStart() is {Col1.Indexing}."); Console.WriteLine($"Date Value"); for (int x = 0; x < Col1.Count; x++) { Console.WriteLine($"{Col1.Date[x]:d} {Col1[x]}"); }
PrimarySeries = Col1.ToBarSeries(); }
protected override void OnBarClose() { if (IsFirstBar) { Console.WriteLine($"Indexing in OnBarClose() is {Col1.Indexing}."); Console.WriteLine("Date Last [0] Prev [1]"); }
Console.WriteLine($"{CurrentDate:d} {Col1.Last} {Col1[0]} {Col1.Previous} {Col1[1]}"); }}When running the code, we’ll see the following printed to the console:
Indexing in OnStrategyStart() is Absolute.Date Value1/1/2024 11/2/2024 21/3/2024 31/4/2024 41/5/2025 5Indexing in OnBarClose() is RelativeToCurrent.Date Last [0] Prev [1]1/2/2024 2 2 1 11/3/2024 3 3 2 21/4/2024 4 4 3 31/5/2025 5 5 4 4We added five values to the pre-defined TimeSeries variable Col1 and then looped through those values from 0 to count - 1. Note how the behavior of Col1[0] changes when called from within OnBarClose. Here [0] always refers to the latest value available relative to the current date being tested not the first value in the series. You can also see the equivalence of Col1.Last and Col1[0] (the current bar being tested) as well as Col1.Previous and Col1[1] (one bar prior to the current bar).
Setback
Section titled “Setback”The eagle-eyed among you may have noticed that we are missing the observation for 1/1/2024 in OnBarClose(). This is due to the Setback property which allows for an additional period of initialization before OnBarClose is called for the first time. By default, the backtester uses a Setback of 1 so that we can always refer to the previous bar without encountering an error. You can modify this behavior by changing the Setback property from within OnStrategyStart() as needed.
MaxBarsBack and FirstValidDate
Section titled “MaxBarsBack and FirstValidDate”Now let’s modify this code further by assigning a 2 period SMA to the Col2 variable:
class ConcurrencyExample2 : Strategy{ protected override void OnStrategyStart() { Col1 = new TimeSeries() { {new DateTime(2024, 1, 1), 1}, {new DateTime(2024, 1, 2), 2}, {new DateTime(2024, 1, 3), 3}, {new DateTime(2024, 1, 4), 4}, {new DateTime(2025, 1, 5), 5} };
Col2 = Sma(Col1, 2); //A simple moving average of length 2 isn't valid until at least 2 bars Setback = 1; //this is the default setback value
Console.WriteLine($"Indexing in OnStrategyStart() is {Col1.Indexing}."); Console.WriteLine($"Date Value"); for (int x = 0; x < Col1.Count; x++) { Console.WriteLine($"{Col1.Date[x]:d} {Col1[x]}"); }
PrimarySeries = Col1.ToBarSeries(); }
protected override void OnBarClose() { if (IsFirstBar) { Console.WriteLine($"Indexing in OnBarClose() {Col1.Indexing}."); Console.WriteLine("Date Last [0] Prev [1] Sma Sma[1]"); }
Console.WriteLine($"{CurrentDate:d} {Col1.Last} {Col1[0]} {Col1.Previous} {Col1[1]} {Col2.Last} {Col2.Previous}"); }}If we re-run we get the following output:
Indexing in OnStrategyStart() is Absolute.Date Value1/1/2024 11/2/2024 21/3/2024 31/4/2024 41/5/2025 5Indexing in OnBarClose() is RelativeToCurrent.Date Last [0] Prev [1] Sma Sma[1]1/3/2024 3 3 2 2 2.5 1.51/4/2024 4 4 3 3 3.5 2.51/5/2025 5 5 4 4 4.5 3.5Note how in OnBarClose we have “lost” another observation after adding the two period moving average. This is because Sma(Col1, 2) requires two observations to initialize the calculation. With the default Setback of 1 to allow for referencing the prior bar, we therefore start calling OnBarClose on day 3.
TimeSeries expose a property called MaxBarsBack which indicates the index at which valid data first becomes available. For a two period moving average it takes periods 0 and 1 to calculate a value, so MaxBarsBack would be 1. A related property is FirstValidDate, which returns the first date on which a TimeSeries is valid (in this case it would return 1/2/2024.)
The backtester looks across all indicators initialized in OnStrategyStart() to determine how much data is needed to “warmup” the system. This generally requires no intervention by the user unless you want to index further back in time than one period. For example, if you wanted to reference the value of the 200 period Sma 10 bars ago you would need to change the Setback to 10. However, if you wanted to also include a 50 period Sma and reference it back ten periods from the current bar, this would be fine since the 200 period Sma requires even more data to initialize than the 60 periods required to reference a 50 period Sma 10 bars ago. In most cases the default Setback of 1 will be fine.
Mixing periodicities
Section titled “Mixing periodicities”Concurrency works automatically and across periodicities so you can mix and match daily and weekly data and the backtester will ensure you are always using the latest value that could have been known at that point in time. In the example below, we buy if the close of the most recent bar is below its 5 period SMA and the weekly SMA is greater than the prior week’s reading. If today is Thursday, Col2.Last, which contains weekly data, would refer to the prior Friday (i.e. four trading days ago) and Col2.Previous would be two Fridays ago. If today is Friday, Close.Last, Col1.Last, and Col2.Last would all share Friday’s date since the week just ended. You can observe concurrency in action by setting a breakpoint in OnBarClose() and examining the CurrentDate property of Col1 (daily) and Col2 (weekly) as the backtester calls each date in the PrimarySeries in succession.
class MixedPeriodicities : Strategy{ protected override void OnStrategyStart() { var weekly = PrimarySeries.ToWeekly();
Col1 = Sma(Close, 5); Col2 = Sma(weekly.Close, 52); }
protected override void OnBarClose() { if (Close.Last < Col1.Last && Col2.Last > Col2.Previous) { Buy(); } }}