AIS1 EURUSD Breakout Strategy
This strategy reproduces the original AIS1 "A System: EURUSD Daily Metrics" expert advisor using StockSharp's high-level API. It trades EURUSD breakouts by comparing the current price action to the previous day's range and manages trades with adaptive position sizing plus a four-hour trailing stop.
Strategy Overview
- Market: EURUSD spot/CFD/forex instruments.
- Primary timeframe: Daily candles provide the reference high, low, and close.
- Secondary timeframe: 4-hour candles drive trailing-stop updates and entry checks.
- Direction: Long and short trades are allowed.
- Style: Breakout continuation with volatility-scaled targets and stops.
Trading Logic
- Track the previous completed daily candle. Calculate the midpoint, range, and derived stop/take distances using configurable multipliers (
StopFactor,TakeFactor). - Evaluate every completed 4-hour candle:
- Long entry: Previous daily close is above the midpoint and the 4-hour high breaks above the previous daily high.
- Short entry: Previous daily close is below the midpoint and the 4-hour low breaks below the previous daily low.
- Position size is determined from the current portfolio equity and the configured risk share (
OrderReserve). The volume is rounded to instrument trading steps. - For open positions the strategy applies three layers of exit control:
- Fixed stop-loss at the opposite side of the daily range scaled by
StopFactor. - Fixed take-profit at a distance of
TakeFactor× daily range. - Dynamic trailing stop using the previous 4-hour range multiplied by
TrailFactor. The trailing stop activates only after the trade moves in profit.
- Fixed stop-loss at the opposite side of the daily range scaled by
- A five-second cooldown after any trade or exit mirrors the original EA behaviour and prevents rapid-fire modifications.
Risk Management
OrderReservedefines the fraction of current equity that can be risked on the next trade. If the calculated size is below the instrument minimum, the trade is skipped.AccountReservetracks the peak equity and stops opening or managing trades once the equity drawdown exceedsAccountReserve - OrderReserve(16% with default inputs).- Trailing exits and fixed targets ensure positions are closed even if new trades are blocked by the drawdown guard.
Parameters
| Parameter | Description |
|---|---|
AccountReserve |
Portion of equity excluded from trading, used to compute the allowed drawdown before trading pauses. |
OrderReserve |
Share of equity risked per trade. Determines the maximum loss using the stop distance. |
TakeFactor |
Multiplier applied to the previous daily range to set the take-profit distance. |
StopFactor |
Multiplier applied to the previous daily range to set the stop-loss distance. |
TrailFactor |
Multiplier applied to the previous 4-hour range to move the trailing stop once the position is profitable. |
EntryCandleType |
Candle type (default daily) used for breakout levels. |
TrailCandleType |
Candle type (default 4-hour) used for intraday evaluation and trailing. |
Notes on the Conversion
- The StockSharp version triggers entries and trailing updates on completed 4-hour candles. The original MQL expert advisor reacted to every tick; using candles keeps the logic robust within the high-level API.
- Stop-loss, take-profit, and trailing exits are executed with market orders when the respective price levels are touched inside the processed candle.
- Margin checks from the MQL version are replaced with equity-based sizing to remain platform-neutral while respecting the original risk constraints.
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>
/// Daily breakout strategy converted from the AIS1 expert advisor.
/// Tracks previous day levels, applies a risk based position size and manages trailing exits.
/// </summary>
public class Ais1EurUsdBreakoutStrategy : Strategy
{
private readonly StrategyParam<decimal> _accountReserve;
private readonly StrategyParam<decimal> _orderReserve;
private readonly StrategyParam<decimal> _takeFactor;
private readonly StrategyParam<decimal> _stopFactor;
private readonly StrategyParam<decimal> _trailFactor;
private readonly StrategyParam<DataType> _entryCandleType;
private readonly StrategyParam<DataType> _trailCandleType;
private decimal _prevDayHigh;
private decimal _prevDayLow;
private decimal _prevDayClose;
private decimal _prevTrailRange;
private bool _hasPrevDay;
private bool _hasPrevTrail;
private decimal _entryPrice;
private decimal _longStop;
private decimal _longTake;
private decimal _shortStop;
private decimal _shortTake;
private decimal _longTrail;
private decimal _shortTrail;
private decimal _maxEquity;
private DateTimeOffset _nextActionTime;
private static readonly TimeSpan Cooldown = TimeSpan.FromSeconds(5);
public decimal AccountReserve
{
get => _accountReserve.Value;
set => _accountReserve.Value = value;
}
public decimal OrderReserve
{
get => _orderReserve.Value;
set => _orderReserve.Value = value;
}
public decimal TakeFactor
{
get => _takeFactor.Value;
set => _takeFactor.Value = value;
}
public decimal StopFactor
{
get => _stopFactor.Value;
set => _stopFactor.Value = value;
}
public decimal TrailFactor
{
get => _trailFactor.Value;
set => _trailFactor.Value = value;
}
public DataType EntryCandleType
{
get => _entryCandleType.Value;
set => _entryCandleType.Value = value;
}
public DataType TrailCandleType
{
get => _trailCandleType.Value;
set => _trailCandleType.Value = value;
}
public Ais1EurUsdBreakoutStrategy()
{
_accountReserve = Param(nameof(AccountReserve), 0.2m)
.SetDisplay("Account Reserve", "Equity share kept outside of trading", "Risk")
;
_orderReserve = Param(nameof(OrderReserve), 0.04m)
.SetDisplay("Order Reserve", "Equity share risked per trade", "Risk")
.SetGreaterThanZero()
;
_takeFactor = Param(nameof(TakeFactor), 0.8m)
.SetDisplay("Take Factor", "Daily range multiplier for take profit", "Targets")
.SetGreaterThanZero()
;
_stopFactor = Param(nameof(StopFactor), 1m)
.SetDisplay("Stop Factor", "Daily range multiplier for stop loss", "Targets")
.SetGreaterThanZero()
;
_trailFactor = Param(nameof(TrailFactor), 5m)
.SetDisplay("Trail Factor", "Intraday range multiplier for trailing", "Targets")
.SetGreaterThanZero()
;
_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Entry Candle", "Primary timeframe for breakout levels", "Data");
_trailCandleType = Param(nameof(TrailCandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Trail Candle", "Secondary timeframe for trailing", "Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security is null)
yield break;
yield return (Security, EntryCandleType);
if (TrailCandleType != EntryCandleType)
yield return (Security, TrailCandleType);
}
protected override void OnReseted()
{
base.OnReseted();
ResetPositionState();
_prevDayHigh = 0m;
_prevDayLow = 0m;
_prevDayClose = 0m;
_prevTrailRange = 0m;
_hasPrevDay = false;
_hasPrevTrail = false;
_maxEquity = 0m;
_nextActionTime = DateTimeOffset.MinValue;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetPositionState();
_maxEquity = GetEquity();
_nextActionTime = DateTimeOffset.MinValue;
var dailySubscription = SubscribeCandles(EntryCandleType);
dailySubscription.Bind(ProcessDailyCandle).Start();
var intradaySubscription = SubscribeCandles(TrailCandleType);
intradaySubscription.Bind(ProcessIntradayCandle).Start();
}
private void ProcessDailyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Store the latest completed day to use as breakout reference on the next session.
_prevDayHigh = candle.HighPrice;
_prevDayLow = candle.LowPrice;
_prevDayClose = candle.ClosePrice;
_hasPrevDay = true;
}
private void ProcessIntradayCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Respect the original EA cooldown before issuing another order modification.
if (candle.CloseTime <= _nextActionTime)
{
UpdateTrailRange(candle);
return;
}
var equity = GetEquity();
UpdateMaxEquity(equity);
if (IsDrawdownBreached(equity))
{
UpdateTrailRange(candle);
return;
}
if (!_hasPrevDay)
{
UpdateTrailRange(candle);
return;
}
var dayRange = _prevDayHigh - _prevDayLow;
if (dayRange <= 0m)
{
UpdateTrailRange(candle);
return;
}
var average = (_prevDayHigh + _prevDayLow) / 2m;
var takeDistance = dayRange * TakeFactor;
var stopDistance = dayRange * StopFactor;
var trailRange = _hasPrevTrail ? _prevTrailRange : candle.HighPrice - candle.LowPrice;
var trailDistance = trailRange * TrailFactor;
if (Position != 0m)
{
HandleExistingPosition(candle, trailDistance);
UpdateTrailRange(candle);
return;
}
TryEnterPosition(candle, average, stopDistance, takeDistance);
UpdateTrailRange(candle);
}
private void HandleExistingPosition(ICandleMessage candle, decimal trailDistance)
{
if (Position > 0m)
{
var exitVolume = Math.Abs(Position);
// Respect take profit first so gains are locked immediately.
if (_longTake > 0m && candle.HighPrice >= _longTake)
{
SellMarket();
ResetAfterExit(candle.CloseTime);
return;
}
var trailingStop = _longStop;
// Update trailing stop only after the trade moves into profit.
if (trailDistance > 0m && candle.ClosePrice > _entryPrice)
{
var candidate = candle.ClosePrice - trailDistance;
if (_longTrail == 0m || candidate > _longTrail)
_longTrail = candidate;
}
if (_longTrail > 0m)
trailingStop = trailingStop > 0m ? Math.Max(trailingStop, _longTrail) : _longTrail;
if (trailingStop > 0m && candle.LowPrice <= trailingStop)
{
SellMarket();
ResetAfterExit(candle.CloseTime);
}
}
else if (Position < 0m)
{
var exitVolume = Math.Abs(Position);
if (_shortTake > 0m && candle.LowPrice <= _shortTake)
{
BuyMarket();
ResetAfterExit(candle.CloseTime);
return;
}
var trailingStop = _shortStop;
if (trailDistance > 0m && candle.ClosePrice < _entryPrice)
{
var candidate = candle.ClosePrice + trailDistance;
if (_shortTrail == 0m || candidate < _shortTrail)
_shortTrail = candidate;
}
if (_shortTrail > 0m)
trailingStop = trailingStop > 0m ? Math.Min(trailingStop, _shortTrail) : _shortTrail;
if (trailingStop > 0m && candle.HighPrice >= trailingStop)
{
BuyMarket();
ResetAfterExit(candle.CloseTime);
}
}
}
private void TryEnterPosition(ICandleMessage candle, decimal average, decimal stopDistance, decimal takeDistance)
{
var breakoutUp = _prevDayClose > average && candle.HighPrice > _prevDayHigh;
var breakoutDown = _prevDayClose < average && candle.LowPrice < _prevDayLow;
if (breakoutUp)
{
var entryPrice = candle.ClosePrice;
var stopPrice = _prevDayHigh - stopDistance;
var risk = entryPrice - stopPrice;
if (risk <= 0m)
return;
var volume = CalculatePositionSize(risk);
if (volume <= 0m)
return;
BuyMarket();
_entryPrice = entryPrice;
_longStop = stopPrice;
_longTake = entryPrice + takeDistance;
_longTrail = 0m;
_shortStop = 0m;
_shortTake = 0m;
_shortTrail = 0m;
_nextActionTime = candle.CloseTime + Cooldown;
}
else if (breakoutDown)
{
var entryPrice = candle.ClosePrice;
var stopPrice = _prevDayLow + stopDistance;
var risk = stopPrice - entryPrice;
if (risk <= 0m)
return;
var volume = CalculatePositionSize(risk);
if (volume <= 0m)
return;
SellMarket();
_entryPrice = entryPrice;
_shortStop = stopPrice;
_shortTake = entryPrice - takeDistance;
_shortTrail = 0m;
_longStop = 0m;
_longTake = 0m;
_longTrail = 0m;
_nextActionTime = candle.CloseTime + Cooldown;
}
}
private decimal CalculatePositionSize(decimal riskPerUnit)
{
if (riskPerUnit <= 0m)
return 0m;
var equity = GetEquity();
if (equity <= 0m)
return 0m;
var maxRisk = equity * OrderReserve;
if (maxRisk <= 0m)
return 0m;
var rawSize = maxRisk / riskPerUnit;
if (rawSize <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 1m;
var minVolume = Security?.MinVolume ?? step;
var maxVolume = Security?.MaxVolume ?? Math.Max(minVolume, step * 1000m);
var steps = Math.Floor(rawSize / step);
var volume = steps * step;
if (volume < minVolume)
{
if (rawSize >= minVolume)
volume = minVolume;
else
return 0m;
}
if (volume > maxVolume)
volume = maxVolume;
return volume;
}
private void UpdateTrailRange(ICandleMessage candle)
{
_prevTrailRange = candle.HighPrice - candle.LowPrice;
_hasPrevTrail = true;
}
private void ResetAfterExit(DateTimeOffset time)
{
ResetPositionState();
_nextActionTime = time + Cooldown;
}
private void ResetPositionState()
{
_entryPrice = 0m;
_longStop = 0m;
_longTake = 0m;
_shortStop = 0m;
_shortTake = 0m;
_longTrail = 0m;
_shortTrail = 0m;
}
private void UpdateMaxEquity(decimal equity)
{
if (equity > _maxEquity)
_maxEquity = equity;
}
private bool IsDrawdownBreached(decimal equity)
{
if (_maxEquity <= 0m)
return false;
var drawdownLimit = AccountReserve - OrderReserve;
if (drawdownLimit <= 0m)
return false;
var threshold = _maxEquity * (1m - drawdownLimit);
return equity < threshold;
}
private decimal GetEquity()
{
return Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
}
}
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.Strategies import Strategy
class ais1_eur_usd_breakout_strategy(Strategy):
def __init__(self):
super(ais1_eur_usd_breakout_strategy, self).__init__()
self._account_reserve = self.Param("AccountReserve", 0.2)
self._order_reserve = self.Param("OrderReserve", 0.04)
self._take_factor = self.Param("TakeFactor", 0.8)
self._stop_factor = self.Param("StopFactor", 1.0)
self._trail_factor = self.Param("TrailFactor", 5.0)
self._entry_candle_type = self.Param("EntryCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._trail_candle_type = self.Param("TrailCandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._prev_day_high = 0.0
self._prev_day_low = 0.0
self._prev_day_close = 0.0
self._prev_trail_range = 0.0
self._has_prev_day = False
self._has_prev_trail = False
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._long_trail = 0.0
self._short_trail = 0.0
self._max_equity = 0.0
self._next_action_time = None
@property
def CandleType(self):
return self._entry_candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._entry_candle_type.Value = value
def OnStarted2(self, time):
super(ais1_eur_usd_breakout_strategy, self).OnStarted2(time)
self._reset_position_state()
self._prev_day_high = 0.0
self._prev_day_low = 0.0
self._prev_day_close = 0.0
self._prev_trail_range = 0.0
self._has_prev_day = False
self._has_prev_trail = False
self._max_equity = self._get_equity()
self._next_action_time = None
daily_sub = self.SubscribeCandles(self._entry_candle_type.Value)
daily_sub.Bind(self._process_daily_candle).Start()
intraday_sub = self.SubscribeCandles(self._trail_candle_type.Value)
intraday_sub.Bind(self._process_intraday_candle).Start()
def _process_daily_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._prev_day_high = float(candle.HighPrice)
self._prev_day_low = float(candle.LowPrice)
self._prev_day_close = float(candle.ClosePrice)
self._has_prev_day = True
def _process_intraday_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._next_action_time is not None and candle.CloseTime <= self._next_action_time:
self._update_trail_range(candle)
return
equity = self._get_equity()
self._update_max_equity(equity)
if self._is_drawdown_breached(equity):
self._update_trail_range(candle)
return
if not self._has_prev_day:
self._update_trail_range(candle)
return
day_range = self._prev_day_high - self._prev_day_low
if day_range <= 0.0:
self._update_trail_range(candle)
return
average = (self._prev_day_high + self._prev_day_low) / 2.0
take_distance = day_range * float(self._take_factor.Value)
stop_distance = day_range * float(self._stop_factor.Value)
trail_r = self._prev_trail_range if self._has_prev_trail else (float(candle.HighPrice) - float(candle.LowPrice))
trail_distance = trail_r * float(self._trail_factor.Value)
if self.Position != 0:
self._handle_existing_position(candle, trail_distance)
self._update_trail_range(candle)
return
self._try_enter_position(candle, average, stop_distance, take_distance)
self._update_trail_range(candle)
def _handle_existing_position(self, candle, trail_distance):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
if self._long_take > 0.0 and high >= self._long_take:
self.SellMarket()
self._reset_after_exit(candle.CloseTime)
return
trailing_stop = self._long_stop
if trail_distance > 0.0 and close > self._entry_price:
candidate = close - trail_distance
if self._long_trail == 0.0 or candidate > self._long_trail:
self._long_trail = candidate
if self._long_trail > 0.0:
if trailing_stop > 0.0:
trailing_stop = max(trailing_stop, self._long_trail)
else:
trailing_stop = self._long_trail
if trailing_stop > 0.0 and low <= trailing_stop:
self.SellMarket()
self._reset_after_exit(candle.CloseTime)
elif self.Position < 0:
if self._short_take > 0.0 and low <= self._short_take:
self.BuyMarket()
self._reset_after_exit(candle.CloseTime)
return
trailing_stop = self._short_stop
if trail_distance > 0.0 and close < self._entry_price:
candidate = close + trail_distance
if self._short_trail == 0.0 or candidate < self._short_trail:
self._short_trail = candidate
if self._short_trail > 0.0:
if trailing_stop > 0.0:
trailing_stop = min(trailing_stop, self._short_trail)
else:
trailing_stop = self._short_trail
if trailing_stop > 0.0 and high >= trailing_stop:
self.BuyMarket()
self._reset_after_exit(candle.CloseTime)
def _try_enter_position(self, candle, average, stop_distance, take_distance):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
breakout_up = self._prev_day_close > average and high > self._prev_day_high
breakout_down = self._prev_day_close < average and low < self._prev_day_low
if breakout_up:
entry_price = close
stop_price = self._prev_day_high - stop_distance
risk = entry_price - stop_price
if risk <= 0.0:
return
volume = self._calculate_position_size(risk)
if volume <= 0.0:
return
self.BuyMarket()
self._entry_price = entry_price
self._long_stop = stop_price
self._long_take = entry_price + take_distance
self._long_trail = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._short_trail = 0.0
self._next_action_time = candle.CloseTime.Add(TimeSpan.FromSeconds(5))
elif breakout_down:
entry_price = close
stop_price = self._prev_day_low + stop_distance
risk = stop_price - entry_price
if risk <= 0.0:
return
volume = self._calculate_position_size(risk)
if volume <= 0.0:
return
self.SellMarket()
self._entry_price = entry_price
self._short_stop = stop_price
self._short_take = entry_price - take_distance
self._short_trail = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._long_trail = 0.0
self._next_action_time = candle.CloseTime.Add(TimeSpan.FromSeconds(5))
def _calculate_position_size(self, risk_per_unit):
if risk_per_unit <= 0.0:
return 0.0
equity = self._get_equity()
if equity <= 0.0:
return 0.0
max_risk = equity * float(self._order_reserve.Value)
if max_risk <= 0.0:
return 0.0
raw_size = max_risk / risk_per_unit
if raw_size <= 0.0:
return 0.0
sec = self.Security
step = float(sec.VolumeStep) if sec is not None and sec.VolumeStep is not None else 1.0
min_volume = float(sec.MinVolume) if sec is not None and sec.MinVolume is not None else step
max_volume = float(sec.MaxVolume) if sec is not None and sec.MaxVolume is not None else max(min_volume, step * 1000.0)
import math
steps = math.floor(raw_size / step)
volume = steps * step
if volume < min_volume:
if raw_size >= min_volume:
volume = min_volume
else:
return 0.0
if volume > max_volume:
volume = max_volume
return volume
def _update_trail_range(self, candle):
self._prev_trail_range = float(candle.HighPrice) - float(candle.LowPrice)
self._has_prev_trail = True
def _reset_after_exit(self, time):
self._reset_position_state()
self._next_action_time = time.Add(TimeSpan.FromSeconds(5))
def _reset_position_state(self):
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._long_trail = 0.0
self._short_trail = 0.0
def _update_max_equity(self, equity):
if equity > self._max_equity:
self._max_equity = equity
def _is_drawdown_breached(self, equity):
if self._max_equity <= 0.0:
return False
drawdown_limit = float(self._account_reserve.Value) - float(self._order_reserve.Value)
if drawdown_limit <= 0.0:
return False
threshold = self._max_equity * (1.0 - drawdown_limit)
return equity < threshold
def _get_equity(self):
pf = self.Portfolio
if pf is None:
return 0.0
if pf.CurrentValue is not None:
return float(pf.CurrentValue)
if pf.BeginValue is not None:
return float(pf.BeginValue)
return 0.0
def OnReseted(self):
super(ais1_eur_usd_breakout_strategy, self).OnReseted()
self._reset_position_state()
self._prev_day_high = 0.0
self._prev_day_low = 0.0
self._prev_day_close = 0.0
self._prev_trail_range = 0.0
self._has_prev_day = False
self._has_prev_trail = False
self._max_equity = 0.0
self._next_action_time = None
def CreateClone(self):
return ais1_eur_usd_breakout_strategy()