Full example: SMA cross
The strategy draws two simple moving averages (SMA) on the chart: a short one (21 bars by default) and a long one (75 bars by default). When the short SMA crosses the long one upwards, the strategy buys; when it crosses downwards, the strategy sells. If the strategy holds an opposite position at that moment, the order volume includes it, so the position is reversed; a signal in the direction of the current position is ignored. The signals are checked only after the strategy has been started and only once per bar: when a new bar opens, the strategy compares the two last closed bars. When the strategy is stopped and Close position on stopping is enabled, it closes its position with a market order.
The strategy composes the built-in SMA of ATAS.Indicators.Technical, as a chart indicator does
(Composite indicators); its settings are shown in the strategy settings and, marked
with [Parameter], in the strategy name on the chart.
Signals
OnCalculate calculates both averages for every bar and trades only on the last bar of a started strategy, once per
bar:
protected override void OnCalculate(int bar, decimal value)
{
var shortSma = _shortSma.Calculate(bar, value);
var longSma = _longSma.Calculate(bar, value);
DataSeries[0][bar] = shortSma;
DataSeries[1][bar] = longSma;
var prevBar = _lastBar;
_lastBar = bar;
// Trade only when the strategy is started, on the last bar,
// and only once per bar: when a new bar opens.
if (!CanProcess(bar) || prevBar == bar)
return;
if (_shortSma[prevBar - 1] < _longSma[prevBar - 1] && _shortSma[prevBar] >= _longSma[prevBar])
{
// cross up
OpenPosition(OrderDirections.Buy);
}
if (_shortSma[prevBar - 1] > _longSma[prevBar - 1] && _shortSma[prevBar] <= _longSma[prevBar])
{
// cross down
OpenPosition(OrderDirections.Sell);
}
}
Position
A market order of the configured volume plus the opposite position, if any; the closing order is the position volume in the opposite direction:
private void OpenPosition(OrderDirections direction)
{
// A signal in the direction of the current position is ignored.
if (direction == OrderDirections.Buy && CurrentPosition > 0 ||
direction == OrderDirections.Sell && CurrentPosition < 0)
return;
var order = new Order
{
Portfolio = Portfolio,
Security = Security,
Direction = direction,
Type = OrderTypes.Market,
// Close the opposite position, if any, and open a new one of the configured volume.
QuantityToFill = Volume + Math.Abs(CurrentPosition),
};
_ = SendOrderAsync(order);
}
private void CloseCurrentPosition()
{
var order = new Order
{
Portfolio = Portfolio,
Security = Security,
Direction = CurrentPosition > 0 ? OrderDirections.Sell : OrderDirections.Buy,
Type = OrderTypes.Market,
QuantityToFill = Math.Abs(CurrentPosition),
};
_ = SendOrderAsync(order);
}
Stopping
OnStopping runs while the strategy is still started, so the closing order is sent:
protected override void OnStopping()
{
// The strategy is still started here, so the closing order is sent.
if (CurrentPosition != 0 && ClosePositionOnStopping)
{
RaiseShowNotification($"Closing current position {CurrentPosition} on stopping.", level: LoggingLevel.Warning);
CloseCurrentPosition();
}
base.OnStopping();
}
The complete strategy
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using ATAS.DataFeedsCore;
using ATAS.Indicators;
using ATAS.Indicators.Technical;
using ATAS.Strategies.Chart;
using Utils.Common.Logging;
namespace ATAS.Docs.Samples.ChartStrategies;
[DisplayName("SMA cross strategy")]
[Category("Samples")]
public class SmaCrossStrategy : ChartStrategy
{
private readonly SMA _shortSma = new();
private readonly SMA _longSma = new();
private int _lastBar;
[Display(Name = "Short period", Order = 10)]
[OFT.Attributes.Parameter]
public int ShortPeriod
{
get => _shortSma.Period;
set
{
_shortSma.Period = Math.Max(1, value);
RecalculateValues();
}
}
[Display(Name = "Long period", Order = 20)]
[OFT.Attributes.Parameter]
public int LongPeriod
{
get => _longSma.Period;
set
{
_longSma.Period = Math.Max(1, value);
RecalculateValues();
}
}
[Display(Name = "Volume", Order = 30)]
[OFT.Attributes.Parameter]
public decimal Volume { get; set; }
[Display(Name = "Close position on stopping", Order = 40)]
[OFT.Attributes.Parameter]
public bool ClosePositionOnStopping { get; set; }
public SmaCrossStrategy()
{
var firstSeries = (ValueDataSeries)DataSeries[0];
firstSeries.Name = "Short";
firstSeries.Color = System.Drawing.Color.Red.Convert();
firstSeries.VisualType = VisualMode.Line;
DataSeries.Add(new ValueDataSeries("Long")
{
VisualType = VisualMode.Line,
Color = System.Drawing.Color.Green.Convert(),
});
ShortPeriod = 21;
LongPeriod = 75;
Volume = 1;
ClosePositionOnStopping = true;
}
protected override void OnCalculate(int bar, decimal value)
{
var shortSma = _shortSma.Calculate(bar, value);
var longSma = _longSma.Calculate(bar, value);
DataSeries[0][bar] = shortSma;
DataSeries[1][bar] = longSma;
var prevBar = _lastBar;
_lastBar = bar;
// Trade only when the strategy is started, on the last bar,
// and only once per bar: when a new bar opens.
if (!CanProcess(bar) || prevBar == bar)
return;
if (_shortSma[prevBar - 1] < _longSma[prevBar - 1] && _shortSma[prevBar] >= _longSma[prevBar])
{
// cross up
OpenPosition(OrderDirections.Buy);
}
if (_shortSma[prevBar - 1] > _longSma[prevBar - 1] && _shortSma[prevBar] <= _longSma[prevBar])
{
// cross down
OpenPosition(OrderDirections.Sell);
}
}
protected override void OnStopping()
{
// The strategy is still started here, so the closing order is sent.
if (CurrentPosition != 0 && ClosePositionOnStopping)
{
RaiseShowNotification($"Closing current position {CurrentPosition} on stopping.", level: LoggingLevel.Warning);
CloseCurrentPosition();
}
base.OnStopping();
}
private void OpenPosition(OrderDirections direction)
{
// A signal in the direction of the current position is ignored.
if (direction == OrderDirections.Buy && CurrentPosition > 0 ||
direction == OrderDirections.Sell && CurrentPosition < 0)
return;
var order = new Order
{
Portfolio = Portfolio,
Security = Security,
Direction = direction,
Type = OrderTypes.Market,
// Close the opposite position, if any, and open a new one of the configured volume.
QuantityToFill = Volume + Math.Abs(CurrentPosition),
};
_ = SendOrderAsync(order);
}
private void CloseCurrentPosition()
{
var order = new Order
{
Portfolio = Portfolio,
Security = Security,
Direction = CurrentPosition > 0 ? OrderDirections.Sell : OrderDirections.Buy,
Type = OrderTypes.Market,
QuantityToFill = Math.Abs(CurrentPosition),
};
_ = SendOrderAsync(order);
}
private async Task SendOrderAsync(Order order)
{
try
{
await OpenOrderAsync(order);
}
catch (Exception ex)
{
this.LogError($"Order {order.Direction} {order.QuantityToFill} error.", ex);
}
}
}