using System;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Awesome Oscillator swing strategy converted from the "Executor AO" MetaTrader expert.
/// Implements the saucer-based entry logic with optional stop, take-profit, and trailing exit management.
/// </summary>
public class ExecutorAoStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _aoShortPeriod;
private readonly StrategyParam<int> _aoLongPeriod;
private readonly StrategyParam<decimal> _minimumAoIndent;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private static readonly object _sync = new();
private AwesomeOscillator _ao = null!;
private decimal? _currentAo;
private decimal? _previousAo;
private decimal? _previousAo2;
private decimal _pipSize;
private decimal? _longEntryPrice;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortEntryPrice;
private decimal? _shortStop;
private decimal? _shortTake;
/// <summary>
/// Initializes a new instance of the <see cref="ExecutorAoStrategy"/> class.
/// </summary>
public ExecutorAoStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Fixed order size", "Risk")
;
_aoShortPeriod = Param(nameof(AoShortPeriod), 5)
.SetDisplay("AO Short Period", "Fast period for Awesome Oscillator", "Indicators")
;
_aoLongPeriod = Param(nameof(AoLongPeriod), 34)
.SetDisplay("AO Long Period", "Slow period for Awesome Oscillator", "Indicators")
;
_minimumAoIndent = Param(nameof(MinimumAoIndent), 0.001m)
.SetNotNegative()
.SetDisplay("Minimum AO Indent", "Minimum distance from zero before signals are valid", "Logic")
;
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing distance in pips", "Risk")
;
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Minimum move before trailing adjusts", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe used for analysis", "General");
}
/// <summary>
/// Fixed order volume used for market entries.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Fast period for the Awesome Oscillator calculation.
/// </summary>
public int AoShortPeriod
{
get => _aoShortPeriod.Value;
set => _aoShortPeriod.Value = value;
}
/// <summary>
/// Slow period for the Awesome Oscillator calculation.
/// </summary>
public int AoLongPeriod
{
get => _aoLongPeriod.Value;
set => _aoLongPeriod.Value = value;
}
/// <summary>
/// Minimum absolute AO value required before trades are allowed.
/// </summary>
public decimal MinimumAoIndent
{
get => _minimumAoIndent.Value;
set => _minimumAoIndent.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum step required before the trailing stop moves.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Candle series used to generate signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ao = null!;
_currentAo = null;
_previousAo = null;
_previousAo2 = null;
_pipSize = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
_pipSize = CalculatePipSize();
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");
_ao = new AwesomeOscillator
{
ShortMa = { Length = AoShortPeriod },
LongMa = { Length = AoLongPeriod }
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(candle => ProcessCandle(candle))
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ao);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
lock (_sync)
{
var aoValue = _ao.Process(new CandleIndicatorValue(_ao, candle) { IsFinal = true });
if (!aoValue.IsFinal || _ao == null || !_ao.IsFormed)
return;
var previousAo = _currentAo;
var previousAo2 = _previousAo;
var positionClosed = HandleActivePositions(candle, previousAo);
StoreAoValue(aoValue.ToDecimal());
if (positionClosed || !previousAo.HasValue || !previousAo2.HasValue || !_currentAo.HasValue)
return;
if (Position != 0m)
return;
var current = _currentAo.Value;
var prev = previousAo.Value;
var prev2 = previousAo2.Value;
var indent = MinimumAoIndent;
if (current > prev && prev < prev2 && current <= -indent)
{
OpenLong(candle.ClosePrice);
return;
}
if (current < prev && prev > prev2 && current >= indent)
OpenShort(candle.ClosePrice);
}
}
private bool HandleActivePositions(ICandleMessage candle, decimal? previousAo)
{
if (Position > 0m)
{
_longEntryPrice ??= candle.ClosePrice;
UpdateTrailingForLong(candle);
if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
{
SellMarket(Math.Abs(Position));
ResetLongState();
return true;
}
if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
{
SellMarket(Math.Abs(Position));
ResetLongState();
return true;
}
if (previousAo.HasValue && previousAo.Value > 0m)
{
SellMarket(Math.Abs(Position));
ResetLongState();
return true;
}
}
else if (Position < 0m)
{
_shortEntryPrice ??= candle.ClosePrice;
UpdateTrailingForShort(candle);
if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
{
BuyMarket(Math.Abs(Position));
ResetShortState();
return true;
}
if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
{
BuyMarket(Math.Abs(Position));
ResetShortState();
return true;
}
if (previousAo.HasValue && previousAo.Value < 0m)
{
BuyMarket(Math.Abs(Position));
ResetShortState();
return true;
}
}
else
{
ResetLongState();
ResetShortState();
}
return false;
}
private void OpenLong(decimal price)
{
var volume = GetTradeVolume();
if (volume <= 0m)
return;
BuyMarket(volume);
_longEntryPrice = price;
_longStop = StopLossPips > 0m ? price - StopLossPips * _pipSize : null;
_longTake = TakeProfitPips > 0m ? price + TakeProfitPips * _pipSize : null;
ResetShortState();
}
private void OpenShort(decimal price)
{
var volume = GetTradeVolume();
if (volume <= 0m)
return;
SellMarket(volume);
_shortEntryPrice = price;
_shortStop = StopLossPips > 0m ? price + StopLossPips * _pipSize : null;
_shortTake = TakeProfitPips > 0m ? price - TakeProfitPips * _pipSize : null;
ResetLongState();
}
private void UpdateTrailingForLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_longEntryPrice.HasValue)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var price = candle.ClosePrice;
var entry = _longEntryPrice.Value;
if (price - entry > trailingDistance + trailingStep)
{
var minimalAllowed = price - (trailingDistance + trailingStep);
if (!_longStop.HasValue || _longStop.Value < minimalAllowed)
_longStop = price - trailingDistance;
}
}
private void UpdateTrailingForShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_shortEntryPrice.HasValue)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var price = candle.ClosePrice;
var entry = _shortEntryPrice.Value;
if (entry - price > trailingDistance + trailingStep)
{
var maximalAllowed = price + (trailingDistance + trailingStep);
if (!_shortStop.HasValue || _shortStop.Value > maximalAllowed)
_shortStop = price + trailingDistance;
}
}
private decimal GetTradeVolume()
{
var volume = Volume;
if (volume <= 0m)
volume = TradeVolume;
return volume;
}
private void StoreAoValue(decimal value)
{
_previousAo2 = _previousAo;
_previousAo = _currentAo;
_currentAo = value;
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0m)
priceStep = 1m;
var decimals = GetDecimalPlaces(priceStep);
var factor = decimals == 3 || decimals == 5 ? 10m : 1m;
return priceStep * factor;
}
private static int GetDecimalPlaces(decimal value)
{
value = Math.Abs(value);
if (value == 0m)
return 0;
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
private void ResetLongState()
{
_longEntryPrice = null;
_longStop = null;
_longTake = null;
}
private void ResetShortState()
{
_shortEntryPrice = null;
_shortStop = null;
_shortTake = null;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AwesomeOscillator, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class executor_ao_strategy(Strategy):
def __init__(self):
super(executor_ao_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 1.0) \
.SetDisplay("Trade Volume", "Fixed order size", "Risk")
self._ao_short_period = self.Param("AoShortPeriod", 5) \
.SetDisplay("AO Short Period", "Fast period for Awesome Oscillator", "Indicators")
self._ao_long_period = self.Param("AoLongPeriod", 34) \
.SetDisplay("AO Long Period", "Slow period for Awesome Oscillator", "Indicators")
self._minimum_ao_indent = self.Param("MinimumAoIndent", 0.001) \
.SetDisplay("Minimum AO Indent", "Minimum distance from zero before signals are valid", "Logic")
self._stop_loss_pips = self.Param("StopLossPips", 50.0) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50.0) \
.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 5.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing distance in pips", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0) \
.SetDisplay("Trailing Step (pips)", "Minimum move before trailing adjusts", "Risk")
self._ao = None
self._current_ao = None
self._previous_ao = None
self._previous_ao2 = None
self._pip_size = 0.0
self._long_entry_price = None
self._long_stop = None
self._long_take = None
self._short_entry_price = None
self._short_stop = None
self._short_take = None
@property
def trade_volume(self):
return self._trade_volume.Value
@property
def ao_short_period(self):
return self._ao_short_period.Value
@property
def ao_long_period(self):
return self._ao_long_period.Value
@property
def minimum_ao_indent(self):
return self._minimum_ao_indent.Value
@property
def stop_loss_pips(self):
return self._stop_loss_pips.Value
@property
def take_profit_pips(self):
return self._take_profit_pips.Value
@property
def trailing_stop_pips(self):
return self._trailing_stop_pips.Value
@property
def trailing_step_pips(self):
return self._trailing_step_pips.Value
def OnReseted(self):
super(executor_ao_strategy, self).OnReseted()
self._ao = None
self._current_ao = None
self._previous_ao = None
self._previous_ao2 = None
self._pip_size = 0.0
self._reset_long_state()
self._reset_short_state()
def OnStarted2(self, time):
super(executor_ao_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._ao = AwesomeOscillator()
self._ao.ShortMa.Length = self.ao_short_period
self._ao.LongMa.Length = self.ao_long_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(2)))
subscription.Bind(self._process_candle)
subscription.Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ao is None:
return
ao_result = self._ao.Process(CandleIndicatorValue(self._ao, candle))
if not self._ao.IsFormed:
return
previous_ao = self._current_ao
previous_ao2 = self._previous_ao
position_closed = self._handle_active_positions(candle, previous_ao)
self._store_ao_value(float(ao_result))
if position_closed:
return
if previous_ao is None or previous_ao2 is None or self._current_ao is None:
return
if self.Position != 0:
return
current = self._current_ao
prev = previous_ao
prev2 = previous_ao2
indent = float(self.minimum_ao_indent)
# Saucer buy: AO dips then rises, current <= -indent
if current > prev and prev < prev2 and current <= -indent:
self._open_long(float(candle.ClosePrice))
return
# Saucer sell: AO peaks then falls, current >= indent
if current < prev and prev > prev2 and current >= indent:
self._open_short(float(candle.ClosePrice))
def _handle_active_positions(self, candle, previous_ao):
if self.Position > 0:
if self._long_entry_price is None:
self._long_entry_price = float(candle.ClosePrice)
self._update_trailing_long(candle)
if self._long_take is not None and float(candle.HighPrice) >= self._long_take:
self.SellMarket()
self._reset_long_state()
return True
if self._long_stop is not None and float(candle.LowPrice) <= self._long_stop:
self.SellMarket()
self._reset_long_state()
return True
if previous_ao is not None and previous_ao > 0.0:
self.SellMarket()
self._reset_long_state()
return True
elif self.Position < 0:
if self._short_entry_price is None:
self._short_entry_price = float(candle.ClosePrice)
self._update_trailing_short(candle)
if self._short_take is not None and float(candle.LowPrice) <= self._short_take:
self.BuyMarket()
self._reset_short_state()
return True
if self._short_stop is not None and float(candle.HighPrice) >= self._short_stop:
self.BuyMarket()
self._reset_short_state()
return True
if previous_ao is not None and previous_ao < 0.0:
self.BuyMarket()
self._reset_short_state()
return True
else:
self._reset_long_state()
self._reset_short_state()
return False
def _open_long(self, price):
self.BuyMarket()
self._long_entry_price = price
sl_pips = float(self.stop_loss_pips)
tp_pips = float(self.take_profit_pips)
self._long_stop = price - sl_pips * self._pip_size if sl_pips > 0.0 else None
self._long_take = price + tp_pips * self._pip_size if tp_pips > 0.0 else None
self._reset_short_state()
def _open_short(self, price):
self.SellMarket()
self._short_entry_price = price
sl_pips = float(self.stop_loss_pips)
tp_pips = float(self.take_profit_pips)
self._short_stop = price + sl_pips * self._pip_size if sl_pips > 0.0 else None
self._short_take = price - tp_pips * self._pip_size if tp_pips > 0.0 else None
self._reset_long_state()
def _update_trailing_long(self, candle):
trail_pips = float(self.trailing_stop_pips)
step_pips = float(self.trailing_step_pips)
if trail_pips <= 0.0 or step_pips <= 0.0 or self._long_entry_price is None:
return
trailing_distance = trail_pips * self._pip_size
trailing_step = step_pips * self._pip_size
price = float(candle.ClosePrice)
entry = self._long_entry_price
if price - entry > trailing_distance + trailing_step:
minimal_allowed = price - (trailing_distance + trailing_step)
if self._long_stop is None or self._long_stop < minimal_allowed:
self._long_stop = price - trailing_distance
def _update_trailing_short(self, candle):
trail_pips = float(self.trailing_stop_pips)
step_pips = float(self.trailing_step_pips)
if trail_pips <= 0.0 or step_pips <= 0.0 or self._short_entry_price is None:
return
trailing_distance = trail_pips * self._pip_size
trailing_step = step_pips * self._pip_size
price = float(candle.ClosePrice)
entry = self._short_entry_price
if entry - price > trailing_distance + trailing_step:
maximal_allowed = price + (trailing_distance + trailing_step)
if self._short_stop is None or self._short_stop > maximal_allowed:
self._short_stop = price + trailing_distance
def _store_ao_value(self, value):
self._previous_ao2 = self._previous_ao
self._previous_ao = self._current_ao
self._current_ao = value
def _calculate_pip_size(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
decimals = self._get_decimal_places(step)
factor = 10.0 if decimals == 3 or decimals == 5 else 1.0
return step * factor
def _get_decimal_places(self, value):
value = abs(value)
if value == 0.0:
return 0
count = 0
while value != round(value) and count < 10:
value *= 10.0
count += 1
return count
def _reset_long_state(self):
self._long_entry_price = None
self._long_stop = None
self._long_take = None
def _reset_short_state(self):
self._short_entry_price = None
self._short_stop = None
self._short_take = None
def CreateClone(self):
return executor_ao_strategy()