Table of Contents

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);
        }
    }
}