AIS1 EURUSD Breakout 策略
本策略将 AIS1 “A System: EURUSD Daily Metrics” 专家顾问迁移到 StockSharp 高级 API。它关注 EURUSD 对前一日区间的突破,并结合自适应仓位管理与 4 小时追踪止损。
策略概览
- 标的:EURUSD 外汇现货/差价合约。
- 主时间框架:日线蜡烛用于计算上一交易日的高、低、收盘价。
- 副时间框架:4 小时蜡烛负责触发进场与更新追踪止损。
- 方向:可做多亦可做空。
- 风格:顺势突破,目标与止损均按波动性缩放。
交易逻辑
- 每当新的日线收盘时,记录该日的最高价、最低价与收盘价,通过
StopFactor与TakeFactor计算止损和止盈距离。 - 对每根完成的 4 小时蜡烛进行检查:
- 做多:上一日收盘价位于上一日区间中值之上,并且当前 4 小时蜡烛的最高价突破上一日最高价。
- 做空:上一日收盘价低于区间中值,并且当前 4 小时蜡烛的最低价跌破上一日最低价。
- 仓位规模依据账户当前权益与
OrderReserve风险份额计算,再按交易品种的最小步长进行调整;若无法达到最小成交量,则放弃该信号。 - 开仓后同时应用三个退出机制:
- 按
StopFactor倍数放置固定止损,位置在上一日区间的另一侧。 - 按
TakeFactor倍数设置固定止盈目标。 - 通过上一根 4 小时蜡烛的波幅乘以
TrailFactor计算追踪止损,仅在浮动盈利时生效。
- 按
- 每次开仓或平仓后都会等待 5 秒再处理新的操作,从而与原始 MQL 逻辑保持一致并避免频繁改单。
风险管理
OrderReserve表示单笔交易可承受的最大权益比例,直接决定止损距离对应的资金风险。AccountReserve记录账户历史最高权益,一旦当前权益回撤超过AccountReserve - OrderReserve(默认约为 16%),策略将暂停新的交易及追踪操作,直至权益恢复。- 即使触发了暂停,已有仓位仍会按照止损、止盈或追踪止损规则在蜡烛内执行市价离场。
参数说明
| 参数 | 含义 |
|---|---|
AccountReserve |
用于计算允许回撤的权益占比,超过该阈值后停止交易。 |
OrderReserve |
单笔交易可投入的权益比例,用于推导仓位大小。 |
TakeFactor |
上一日波幅的倍数,用于设置固定止盈距离。 |
StopFactor |
上一日波幅的倍数,用于设置固定止损距离。 |
TrailFactor |
上一根 4 小时波幅的倍数,用于更新追踪止损。 |
EntryCandleType |
用于计算突破水平的蜡烛类型(默认日线)。 |
TrailCandleType |
用于检查信号与追踪的蜡烛类型(默认 4 小时)。 |
转换说明
- 原始 EA 在每个报价上执行逻辑;此实现改为在 4 小时蜡烛收盘时触发,符合 StockSharp 高级 API 的推荐用法并提升稳定性。
- 止损、止盈与追踪止损在检测到价格穿越目标水平后,通过市价单完成平仓。
- MQL 版本基于保证金的检查被统一替换为权益驱动的风险计算,以便在不同券商/账户环境下保持一致表现。
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()