Your first strategy
Your first strategy
A complete, working strategy: declare parameters and indicators, then go long or flat on a moving-average crossover.
SIMPLE Moving-average crossover
The example declares two moving averages whose periods come from parameters, goes long while the fast average is above the slow one, and exits to flat when the fast average crosses back below. It uses only the SDK packages, so it has two files: a small moving-average indicator and the strategy that declares it twice.
The IsFlat and IsLong guards make the logic edge-triggered:
EnterLong fires only while flat and ExitLong only while long, so the strategy sends
one order per crossover instead of one per bar. The parameter declarations, the
DeclareIndicator factories and the OnBar body are each covered in the pages that
follow.
MovingAverage.csusing SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.Plots;
using SabrTrader.Pipeline.Series;
[IndicatorDescription("Simple moving average of the close.")]
public sealed class MovingAverage : IndicatorBase
{
private readonly ChunkedSeries<double> _values = new();
[IndicatorParameter(DisplayName = "Period", MinValue = 1, MaxValue = 1000, Step = 1)]
public int Period { get; set; } = 20;
/// <summary>Latest value; NaN until Period bars exist.</summary>
public double Value => _values.Count > 0 ? _values[0] : double.NaN;
public MovingAverage(IIndicator? parent = null, ISeries<double>? input = null, int period = 20)
: base(parent, input)
{
Period = period;
AddPlot(new Plot("MA", _values, PlotStyle.Line, new ChartColor(33, 150, 243), 1.5));
}
public override void OnDataLoaded(IIndicatorContext ctx)
{
for (int barsAgo = Input.Count - 1; barsAgo >= 0; barsAgo--)
_values.Append(Average(barsAgo));
}
public override void OnBarUpdate(IIndicatorContext ctx) => _values.Append(Average(0));
private double Average(int barsAgo)
{
if (Input.Count - barsAgo < Period) return double.NaN;
double sum = 0;
for (int k = 0; k < Period; k++) sum += Input[barsAgo + k];
return sum / Period;
}
}
MaCrossStrategy.csusing SabrTrader.Pipeline.Strategies;
using SabrTrader.Pipeline.Strategies.Catalog;
[StrategyMetadata(
DisplayName = "MA Cross",
Description = "Long while a fast moving average is above a slow one; flat when it crosses back below.")]
public sealed class MaCrossStrategy : Strategy
{
private StrategyParameter<int> _fastPeriod = null!;
private StrategyParameter<int> _slowPeriod = null!;
private StrategyParameter<int> _quantity = null!;
private StrategyIndicator<MovingAverage> _fast = null!;
private StrategyIndicator<MovingAverage> _slow = null!;
protected override void OnInitialize()
{
// (name, default, min, max, step): the optimizable form.
_fastPeriod = IntParameter("FastPeriod", 10, 2, 100, 1);
_slowPeriod = IntParameter("SlowPeriod", 30, 5, 400, 1);
_quantity = IntParameter("Quantity", 1, 1, 100, 1);
// The factories run after parameter overrides, so each period is this run's value.
_fast = DeclareIndicator(() => new MovingAverage { Period = _fastPeriod.Value });
_slow = DeclareIndicator(() => new MovingAverage { Period = _slowPeriod.Value });
}
protected override void OnBar()
{
double fast = _fast.Instance.Value;
double slow = _slow.Instance.Value;
if (double.IsNaN(fast) || double.IsNaN(slow)) return; // still warming up
if (fast > slow && IsFlat) EnterLong(_quantity.Value);
else if (fast < slow && IsLong) ExitLong();
}
}
Anatomy of the example
Three pieces, each covered in depth later in the chapter:
Parameters
The IntParameter(name, default, min, max, step)
overload declares an optimizable input. Hold the returned handle and read .Value.
More →
Indicators
DeclareIndicator(factory) feeds an indicator every
bar the strategy sees. Read it through .Instance from OnStart onward.
More →
Managed intent
EnterLong and ExitLong state the
position you want; the framework computes the order and handles reversals.
More →