FatlSatlOsma Strategy
This example reproduces logic of the MetaTrader expert Exp_FatlSatlOsma using StockSharp high level API.
The original system works with the Fatl/Satl oscillator (a custom indicator similar to MACD).
The strategy looks for a change in oscillator direction:
- When the oscillator rises for two bars and the last value is higher than the previous, a long position is opened and short positions are closed.
- When the oscillator falls for two bars and the last value is lower than the previous, a short position is opened and long positions are closed.
The oscillator is implemented through the built-in MovingAverageConvergenceDivergenceSignal indicator with configurable fast and slow periods.
Default values correspond to the original FATL/SATL parameters.
Details
- Entry Criteria: oscillator acceleration.
- Long/Short: both.
- Exit Criteria: opposite acceleration.
- Stops: none.
- Default Values:
Fast= 39Slow= 65CandleType= 12 hour time frame
- Filters:
- Category: Momentum
- Direction: Both
- Indicators: MACD
- Stops: No
- Complexity: Basic
- Timeframe: Medium-term
- Seasonality: No
- Neural networks: No
- Divergence: Yes
- Risk level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Simple MACD momentum strategy inspired by FatlSatlOsma.
/// </summary>
public class FatlSatlOsmaStrategy : Strategy
{
private readonly StrategyParam<int> _fast;
private readonly StrategyParam<int> _slow;
private readonly StrategyParam<bool> _buyOpen;
private readonly StrategyParam<bool> _sellOpen;
private readonly StrategyParam<bool> _buyClose;
private readonly StrategyParam<bool> _sellClose;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prev1, _prev2;
private bool _init;
private int _barsSinceTrade;
public int Fast { get => _fast.Value; set => _fast.Value = value; }
public int Slow { get => _slow.Value; set => _slow.Value = value; }
public bool BuyOpen { get => _buyOpen.Value; set => _buyOpen.Value = value; }
public bool SellOpen { get => _sellOpen.Value; set => _sellOpen.Value = value; }
public bool BuyClose { get => _buyClose.Value; set => _buyClose.Value = value; }
public bool SellClose { get => _sellClose.Value; set => _sellClose.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public FatlSatlOsmaStrategy()
{
_fast = Param(nameof(Fast), 39).SetGreaterThanZero();
_slow = Param(nameof(Slow), 65).SetGreaterThanZero();
_buyOpen = Param(nameof(BuyOpen), true);
_sellOpen = Param(nameof(SellOpen), true);
_buyClose = Param(nameof(BuyClose), true);
_sellClose = Param(nameof(SellClose), true);
_cooldownBars = Param(nameof(CooldownBars), 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_prev1 = _prev2 = 0m;
_init = false;
_barsSinceTrade = CooldownBars;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = Fast },
LongMa = { Length = Slow }
},
SignalMa = { Length = 9 }
};
SubscribeCandles(CandleType)
.BindEx(macd, OnProcess)
.Start();
}
private void OnProcess(ICandleMessage candle, IIndicatorValue macdVal)
{
if (candle.State != CandleStates.Finished)
return;
var macdNullable = ((MovingAverageConvergenceDivergenceSignalValue)macdVal).Macd;
if (macdNullable is not decimal val)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_barsSinceTrade < CooldownBars)
_barsSinceTrade++;
if (!_init)
{
_prev1 = _prev2 = val;
_init = true;
return;
}
if (_prev1 < _prev2)
{
if (_barsSinceTrade >= CooldownBars && BuyOpen && val > _prev1 && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_barsSinceTrade = 0;
}
}
else if (_prev1 > _prev2)
{
if (_barsSinceTrade >= CooldownBars && SellOpen && val < _prev1 && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_barsSinceTrade = 0;
}
}
_prev2 = _prev1;
_prev1 = val;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class fatl_satl_osma_strategy(Strategy):
def __init__(self):
super(fatl_satl_osma_strategy, self).__init__()
self._fast = self.Param("Fast", 39) \
.SetDisplay("Fast", "Fast MA period", "Indicator")
self._slow = self.Param("Slow", 65) \
.SetDisplay("Slow", "Slow MA period", "Indicator")
self._buy_open = self.Param("BuyOpen", True) \
.SetDisplay("Buy Open", "Allow opening long positions", "Trading")
self._sell_open = self.Param("SellOpen", True) \
.SetDisplay("Sell Open", "Allow opening short positions", "Trading")
self._buy_close = self.Param("BuyClose", True) \
.SetDisplay("Buy Close", "Allow closing long positions", "Trading")
self._sell_close = self.Param("SellClose", True) \
.SetDisplay("Sell Close", "Allow closing short positions", "Trading")
self._cooldown_bars = self.Param("CooldownBars", 1) \
.SetDisplay("Cooldown Bars", "Bars to wait after a completed trade", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._prev1 = 0.0
self._prev2 = 0.0
self._init = False
self._bars_since_trade = 0
@property
def Fast(self):
return self._fast.Value
@Fast.setter
def Fast(self, value):
self._fast.Value = value
@property
def Slow(self):
return self._slow.Value
@Slow.setter
def Slow(self, value):
self._slow.Value = value
@property
def BuyOpen(self):
return self._buy_open.Value
@BuyOpen.setter
def BuyOpen(self, value):
self._buy_open.Value = value
@property
def SellOpen(self):
return self._sell_open.Value
@SellOpen.setter
def SellOpen(self, value):
self._sell_open.Value = value
@property
def BuyClose(self):
return self._buy_close.Value
@BuyClose.setter
def BuyClose(self, value):
self._buy_close.Value = value
@property
def SellClose(self):
return self._sell_close.Value
@SellClose.setter
def SellClose(self, value):
self._sell_close.Value = value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@CooldownBars.setter
def CooldownBars(self, value):
self._cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(fatl_satl_osma_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.Fast
macd.Macd.LongMa.Length = self.Slow
macd.SignalMa.Length = 9
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.BindEx(macd, self.ProcessCandle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, macd)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, macd_val):
if candle.State != CandleStates.Finished:
return
macd_raw = macd_val.Macd
if macd_raw is None:
return
val = float(macd_raw)
if self._bars_since_trade < self.CooldownBars:
self._bars_since_trade += 1
if not self._init:
self._prev1 = val
self._prev2 = val
self._init = True
return
if self._prev1 < self._prev2:
if (self._bars_since_trade >= self.CooldownBars
and self.BuyOpen and val > self._prev1
and self.Position <= 0):
self.BuyMarket(self.Volume + abs(self.Position))
self._bars_since_trade = 0
elif self._prev1 > self._prev2:
if (self._bars_since_trade >= self.CooldownBars
and self.SellOpen and val < self._prev1
and self.Position >= 0):
self.SellMarket(self.Volume + abs(self.Position))
self._bars_since_trade = 0
self._prev2 = self._prev1
self._prev1 = val
def OnReseted(self):
super(fatl_satl_osma_strategy, self).OnReseted()
self._prev1 = 0.0
self._prev2 = 0.0
self._init = False
self._bars_since_trade = self.CooldownBars
def CreateClone(self):
return fatl_satl_osma_strategy()