Crossing of Two iMA 策略
该策略将 MetaTrader 5 上广受欢迎的 “Crossing of two iMA” 专家顾问完整迁移到 StockSharp 的高级 API。系统在两条可配置均线发生交叉时交易,并可选地通过第三条均线确认趋势方向。与原始 EA 一样,策略支持手动下单手数或基于账户风险百分比的动态仓位、PriceLevelPips 参数控制的类挂单入场方式,以及带有步进的追踪止损。
所有信号都在收盘完成的 K 线处计算,实现了 EA “等待新柱子” 的逻辑。PriceLevelPips 对应的挂单行为在内部模拟:策略跟踪蜡烛的最高价和最低价,当价格触发预设水平时立即入场,因此不会向交易所发送真实的 stop/limit 订单。多单触发条件分别对应买入止损和买入限价,空头触发条件与之对称。
交易规则
- 指标
- 第一条均线(周期、移位与算法均可配置)。
- 第二条均线(同样可配置)。
- 可选的第三条均线过滤器(
UseThirdMovingAverage = true)。
- 入场条件
- 主交叉(柱 0 与柱 1)
- 多头:当前柱第一均线从下向上穿过第二均线,上一柱仍位于下方。如果启用了过滤器,则第三均线必须低于第一均线。
- 空头:第一均线从上向下穿过第二均线,若启用过滤器,则第三均线需高于第一均线。
- 补充交叉(柱 0 与柱 2)
- 捕捉在前两根柱之间发生的快速交叉。如果最近三根柱内已开仓,则忽略该信号,等价于原始 EA 的历史检查逻辑。
- 主交叉(柱 0 与柱 1)
- 方向:可做多亦可做空。
- 止损与目标
- 止损和止盈以点(pip)为单位输入,随后根据品种最小报价步长转换成价格距离,并对 3/5 位小数的货币对做与 EA 相同的调整。
- 当
TrailingStopPips > 0时启用追踪止损;价格每向有利方向推进至少TrailingStepPips点后,止损就向前移动固定距离。
PriceLevelPips行为0:立即市价进场。< 0:模拟止损挂单(多单高于当前价,空单低于当前价),止损和止盈同时按同样幅度平移。> 0:模拟限价挂单(多单低于当前价,空单高于当前价),保护价位相应平移。
资金管理
UseFixedVolume = true时完全复现 EA 的“手动手数”模式:使用策略参数Volume,并在开新仓前平掉反向仓位。- 当
UseFixedVolume = false时,仓位大小按照Portfolio.CurrentValue * RiskPercent / 100计算风险金额,再除以止损距离。如果未设置止损(StopLossPips = 0),风险距离为零,策略将拒绝开仓,这与 MQL5 中MoneyFixedRisk返回零手数的结果一致。
追踪止损
- 多单:当价格至少向有利方向移动
TrailingStepPips点后,止损更新为Close - TrailingStopPips * pipValue,且不会下移。 - 空单:对称地更新为
Close + TrailingStopPips * pipValue,确保止损始终向盈利方向收紧。 - 在调整追踪止损前,先检测是否触发初始止损或止盈,以保持与原策略相同的优先级。
默认参数
- 第一均线:周期
5、移位3、方法Smoothed。 - 第二均线:周期
8、移位5、方法Smoothed。 - 第三均线过滤器:开启,周期
13、移位8、方法Smoothed。 - 风险控制:止损
50点、止盈50点、追踪10点,步进4点。 - 资金管理:默认
UseFixedVolume = true,RiskPercent = 5供动态手数使用。 - 入场偏移:
0点(市价)。 - K 线类型:1 分钟,可根据需要替换成任意周期。
实现说明
- 均线的
shift参数会将指标值延迟指定柱数,因此在 StockSharp 图表上与 MT5 的视觉移位一致。 - 策略只保留最小的状态(当前柱以及前两柱的数值),即可实现原 EA 的「柱 [0], [1], [2]」逻辑,不会创建额外的大型历史集合。
- 每当出现新的信号都会清空待触发的入场,模拟 EA 中的
DeleteAllOrders()调用。 - 由于 StockSharp 的订单执行是异步的,策略记录的入场价使用目标触发价来计算追踪止损和止盈位置。在以蜡烛数据回测时,这样即可重现原 EA 的行为,无需依赖逐笔成交。
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>
/// Strategy that emulates the "Crossing of two iMA" MQL5 expert advisor.
/// It trades crossovers between two configurable moving averages with an optional third filter average.
/// Supports manual volume or percentage risk based sizing, simulated pending orders and trailing stop management.
/// </summary>
public class CrossingOfTwoIMAStrategy : Strategy
{
/// <summary>
/// Moving average calculation methods supported by the strategy.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed (RMA) moving average.
/// </summary>
Smoothed,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Weighted,
}
private readonly StrategyParam<int> _firstPeriod;
private readonly StrategyParam<int> _firstShift;
private readonly StrategyParam<MovingAverageMethods> _firstMethod;
private readonly StrategyParam<int> _secondPeriod;
private readonly StrategyParam<int> _secondShift;
private readonly StrategyParam<MovingAverageMethods> _secondMethod;
private readonly StrategyParam<bool> _useThirdAverage;
private readonly StrategyParam<int> _thirdPeriod;
private readonly StrategyParam<int> _thirdShift;
private readonly StrategyParam<MovingAverageMethods> _thirdMethod;
private readonly StrategyParam<bool> _useFixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _priceLevelPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private DecimalLengthIndicator _firstMa;
private DecimalLengthIndicator _secondMa;
private DecimalLengthIndicator _thirdMa;
private readonly List<decimal> _firstValues = new();
private readonly List<decimal> _secondValues = new();
private readonly List<decimal> _thirdValues = new();
private readonly List<DateTimeOffset> _openTimes = new();
private decimal _pipSize;
private decimal? _entryPrice;
private decimal? _activeStopLoss;
private decimal? _activeTakeProfit;
private bool _isLongPosition;
private PendingOrder _pendingOrder;
private DateTimeOffset? _lastEntryTime;
private enum PendingOrderTypes
{
None,
BuyStop,
BuyLimit,
SellStop,
SellLimit,
}
private sealed class PendingOrder
{
public PendingOrderTypes Type { get; init; }
public decimal EntryPrice { get; init; }
public decimal? StopLoss { get; init; }
public decimal? TakeProfit { get; init; }
public decimal Volume { get; init; }
}
/// <summary>
/// Initializes a new instance of the <see cref="CrossingOfTwoIMAStrategy"/> class.
/// </summary>
public CrossingOfTwoIMAStrategy()
{
_firstPeriod = Param(nameof(FirstMaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("First MA Period", "Period of the first moving average", "First Moving Average")
.SetOptimize(2, 30, 1);
_firstShift = Param(nameof(FirstMaShift), 3)
.SetNotNegative()
.SetDisplay("First MA Shift", "Shift applied to the first moving average", "First Moving Average");
_firstMethod = Param(nameof(FirstMaMethod), MovingAverageMethods.Simple)
.SetDisplay("First MA Method", "Calculation method of the first moving average", "First Moving Average");
_secondPeriod = Param(nameof(SecondMaPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("Second MA Period", "Period of the second moving average", "Second Moving Average")
.SetOptimize(3, 60, 1);
_secondShift = Param(nameof(SecondMaShift), 5)
.SetNotNegative()
.SetDisplay("Second MA Shift", "Shift applied to the second moving average", "Second Moving Average");
_secondMethod = Param(nameof(SecondMaMethod), MovingAverageMethods.Simple)
.SetDisplay("Second MA Method", "Calculation method of the second moving average", "Second Moving Average");
_useThirdAverage = Param(nameof(UseThirdMovingAverage), true)
.SetDisplay("Use Third MA", "Enable the third moving average as a directional filter", "Third Moving Average");
_thirdPeriod = Param(nameof(ThirdMaPeriod), 13)
.SetGreaterThanZero()
.SetDisplay("Third MA Period", "Period of the third moving average", "Third Moving Average");
_thirdShift = Param(nameof(ThirdMaShift), 8)
.SetNotNegative()
.SetDisplay("Third MA Shift", "Shift applied to the third moving average", "Third Moving Average");
_thirdMethod = Param(nameof(ThirdMaMethod), MovingAverageMethods.Simple)
.SetDisplay("Third MA Method", "Calculation method of the third moving average", "Third Moving Average");
_useFixedVolume = Param(nameof(UseFixedVolume), true)
.SetDisplay("Use Fixed Volume", "Use the strategy volume directly instead of risk based sizing", "Money Management");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetNotNegative()
.SetDisplay("Risk %", "Risk percentage of portfolio value per trade when position sizing is dynamic", "Money Management");
_priceLevelPips = Param(nameof(PriceLevelPips), 0)
.SetDisplay("Price Level (pips)", "Offset in pips for simulated pending orders (negative for stop, positive for limit)", "Orders");
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Initial stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 10)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 4)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional progress in pips required before the trailing stop is advanced", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Primary candle series used for signals", "General");
}
/// <summary>
/// Period of the first moving average.
/// </summary>
public int FirstMaPeriod
{
get => _firstPeriod.Value;
set => _firstPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the first moving average.
/// </summary>
public int FirstMaShift
{
get => _firstShift.Value;
set => _firstShift.Value = value;
}
/// <summary>
/// Method used for the first moving average.
/// </summary>
public MovingAverageMethods FirstMaMethod
{
get => _firstMethod.Value;
set => _firstMethod.Value = value;
}
/// <summary>
/// Period of the second moving average.
/// </summary>
public int SecondMaPeriod
{
get => _secondPeriod.Value;
set => _secondPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the second moving average.
/// </summary>
public int SecondMaShift
{
get => _secondShift.Value;
set => _secondShift.Value = value;
}
/// <summary>
/// Method used for the second moving average.
/// </summary>
public MovingAverageMethods SecondMaMethod
{
get => _secondMethod.Value;
set => _secondMethod.Value = value;
}
/// <summary>
/// Enables the third moving average filter.
/// </summary>
public bool UseThirdMovingAverage
{
get => _useThirdAverage.Value;
set => _useThirdAverage.Value = value;
}
/// <summary>
/// Period of the third moving average.
/// </summary>
public int ThirdMaPeriod
{
get => _thirdPeriod.Value;
set => _thirdPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the third moving average.
/// </summary>
public int ThirdMaShift
{
get => _thirdShift.Value;
set => _thirdShift.Value = value;
}
/// <summary>
/// Method used for the third moving average.
/// </summary>
public MovingAverageMethods ThirdMaMethod
{
get => _thirdMethod.Value;
set => _thirdMethod.Value = value;
}
/// <summary>
/// Use fixed volume or percentage based sizing.
/// </summary>
public bool UseFixedVolume
{
get => _useFixedVolume.Value;
set => _useFixedVolume.Value = value;
}
/// <summary>
/// Risk percentage per trade when dynamic sizing is active.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Offset in pips that defines simulated pending order behavior.
/// </summary>
public int PriceLevelPips
{
get => _priceLevelPips.Value;
set => _priceLevelPips.Value = value;
}
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Required additional progress (in pips) before advancing the trailing stop.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Primary candle type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_firstValues.Clear();
_secondValues.Clear();
_thirdValues.Clear();
_openTimes.Clear();
_entryPrice = null;
_activeStopLoss = null;
_activeTakeProfit = null;
_isLongPosition = false;
_pendingOrder = null;
_lastEntryTime = null;
_pipSize = 0m;
_firstMa = null;
_secondMa = null;
_thirdMa = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
_thirdMa = UseThirdMovingAverage ? CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod) : null;
_firstValues.Clear();
_secondValues.Clear();
_thirdValues.Clear();
_openTimes.Clear();
_pipSize = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals;
if (decimals == 3 || decimals == 5)
_pipSize *= 10m;
var subscription = SubscribeCandles(CandleType);
if (UseThirdMovingAverage && _thirdMa != null)
{
subscription
.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle)
.Start();
}
else
{
subscription
.Bind(_firstMa, _secondMa, ProcessCandle)
.Start();
}
}
private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue)
{
ProcessCandleInternal(candle, firstValue, secondValue, null);
}
private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
{
ProcessCandleInternal(candle, firstValue, secondValue, thirdValue);
}
private void ProcessCandleInternal(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal? thirdValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateOpenTimes(candle.OpenTime);
HandlePendingOrders(candle);
var positionChanged = false;
ManageActivePosition(candle, ref positionChanged);
UpdateSeries(_firstValues, FirstMaShift, firstValue);
UpdateSeries(_secondValues, SecondMaShift, secondValue);
if (UseThirdMovingAverage && thirdValue.HasValue)
UpdateSeries(_thirdValues, ThirdMaShift, thirdValue.Value);
if (!_firstMa.IsFormed || !_secondMa.IsFormed)
return;
// already checked above
decimal? thirdCurrent = null;
if (UseThirdMovingAverage)
{
if (_thirdMa?.IsFormed != true)
return;
thirdCurrent = GetSeriesValue(_thirdValues, ThirdMaShift, 0);
}
var first0 = GetSeriesValue(_firstValues, FirstMaShift, 0);
var first1 = GetSeriesValue(_firstValues, FirstMaShift, 1);
var first2 = GetSeriesValue(_firstValues, FirstMaShift, 2);
var second0 = GetSeriesValue(_secondValues, SecondMaShift, 0);
var second1 = GetSeriesValue(_secondValues, SecondMaShift, 1);
var second2 = GetSeriesValue(_secondValues, SecondMaShift, 2);
if (first0 is null || first1 is null || second0 is null || second1 is null)
return;
var priceLevelOffset = Math.Abs(PriceLevelPips) * _pipSize;
var stopLoss = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeProfit = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
var currentOpenTime = candle.OpenTime;
var startTime = GetOpenTime(3) ?? DateTimeOffset.MinValue;
var recentEntry = _lastEntryTime.HasValue && _lastEntryTime.Value >= startTime && _lastEntryTime.Value < currentOpenTime;
if (first0 > second0 && first1 < second1)
{
if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0)
{
EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 < second0 && first1 > second1)
{
if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0)
{
EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 > second0 && first2 is not null && second2 is not null && first2 < second2)
{
if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0))
{
EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 < second2 && first1 > second2 && second2 is not null)
{
if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0))
{
EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
}
}
}
private void EnterLong(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
{
if (Position > 0)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = stopLossOffset > 0m ? entryPrice - stopLossOffset : (decimal?)null;
var takePrice = takeProfitOffset > 0m ? entryPrice + takeProfitOffset : (decimal?)null;
var volume = CalculateOrderVolume(entryPrice, stopPrice);
if (volume <= 0m)
return;
CancelPendingOrders();
if (PriceLevelPips == 0)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0m)
return;
BuyMarket();
_entryPrice = entryPrice;
_activeStopLoss = stopPrice;
_activeTakeProfit = takePrice;
_isLongPosition = true;
_lastEntryTime = candle.OpenTime;
}
else if (PriceLevelPips < 0)
{
var targetPrice = entryPrice + priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.BuyStop,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
else
{
var targetPrice = entryPrice - priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.BuyLimit,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
}
private void EnterShort(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
{
if (Position < 0)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = stopLossOffset > 0m ? entryPrice + stopLossOffset : (decimal?)null;
var takePrice = takeProfitOffset > 0m ? entryPrice - takeProfitOffset : (decimal?)null;
var volume = CalculateOrderVolume(entryPrice, stopPrice);
if (volume <= 0m)
return;
CancelPendingOrders();
if (PriceLevelPips == 0)
{
var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0m)
return;
SellMarket();
_entryPrice = entryPrice;
_activeStopLoss = stopPrice;
_activeTakeProfit = takePrice;
_isLongPosition = false;
_lastEntryTime = candle.OpenTime;
}
else if (PriceLevelPips < 0)
{
var targetPrice = entryPrice - priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.SellStop,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
else
{
var targetPrice = entryPrice + priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.SellLimit,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
}
private void HandlePendingOrders(ICandleMessage candle)
{
if (_pendingOrder is null)
return;
var triggered = _pendingOrder.Type switch
{
PendingOrderTypes.BuyStop => candle.HighPrice >= _pendingOrder.EntryPrice,
PendingOrderTypes.BuyLimit => candle.LowPrice <= _pendingOrder.EntryPrice,
PendingOrderTypes.SellStop => candle.LowPrice <= _pendingOrder.EntryPrice,
PendingOrderTypes.SellLimit => candle.HighPrice >= _pendingOrder.EntryPrice,
_ => false,
};
if (!triggered)
return;
var volume = _pendingOrder.Volume;
if (volume <= 0m)
{
_pendingOrder = null;
return;
}
if (_pendingOrder.Type == PendingOrderTypes.BuyStop || _pendingOrder.Type == PendingOrderTypes.BuyLimit)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume > 0m)
{
BuyMarket();
_entryPrice = _pendingOrder.EntryPrice;
_activeStopLoss = _pendingOrder.StopLoss;
_activeTakeProfit = _pendingOrder.TakeProfit;
_isLongPosition = true;
_lastEntryTime = candle.OpenTime;
}
}
else
{
var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (totalVolume > 0m)
{
SellMarket();
_entryPrice = _pendingOrder.EntryPrice;
_activeStopLoss = _pendingOrder.StopLoss;
_activeTakeProfit = _pendingOrder.TakeProfit;
_isLongPosition = false;
_lastEntryTime = candle.OpenTime;
}
}
_pendingOrder = null;
}
private void ManageActivePosition(ICandleMessage candle, ref bool positionChanged)
{
if (Position == 0)
return;
var positionVolume = Math.Abs(Position);
if (positionVolume <= 0m)
return;
if (_isLongPosition)
{
if (_activeTakeProfit.HasValue && candle.HighPrice >= _activeTakeProfit.Value)
{
SellMarket();
ResetPositionState();
positionChanged = true;
return;
}
if (_activeStopLoss.HasValue && candle.LowPrice <= _activeStopLoss.Value)
{
SellMarket();
ResetPositionState();
positionChanged = true;
return;
}
UpdateTrailingForLong(candle);
}
else
{
if (_activeTakeProfit.HasValue && candle.LowPrice <= _activeTakeProfit.Value)
{
BuyMarket();
ResetPositionState();
positionChanged = true;
return;
}
if (_activeStopLoss.HasValue && candle.HighPrice >= _activeStopLoss.Value)
{
BuyMarket();
ResetPositionState();
positionChanged = true;
return;
}
UpdateTrailingForShort(candle);
}
}
private void UpdateTrailingForLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var targetStop = candle.ClosePrice - trailingDistance;
if (!_activeStopLoss.HasValue || targetStop <= _activeStopLoss.Value)
return;
if (trailingStep <= 0m || _activeStopLoss.Value < targetStop - trailingStep)
_activeStopLoss = targetStop;
}
private void UpdateTrailingForShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var targetStop = candle.ClosePrice + trailingDistance;
if (!_activeStopLoss.HasValue || targetStop >= _activeStopLoss.Value)
return;
if (trailingStep <= 0m || _activeStopLoss.Value > targetStop + trailingStep)
_activeStopLoss = targetStop;
}
private decimal CalculateOrderVolume(decimal entryPrice, decimal? stopPrice)
{
if (UseFixedVolume || !stopPrice.HasValue)
return Volume;
var riskDistance = Math.Abs(entryPrice - stopPrice.Value);
if (riskDistance <= 0m)
return 0m;
var equity = Portfolio?.CurrentValue ?? 0m;
var riskAmount = equity * RiskPercent / 100m;
return riskAmount > 0m ? riskAmount / riskDistance : 0m;
}
private void CancelPendingOrders()
{
_pendingOrder = null;
}
private void ResetPositionState()
{
_entryPrice = null;
_activeStopLoss = null;
_activeTakeProfit = null;
_isLongPosition = false;
}
private void UpdateSeries(List<decimal> values, int shift, decimal value)
{
values.Add(value);
var maxSize = Math.Max(shift + 3, 3);
while (values.Count > maxSize)
values.RemoveAt(0);
}
private static decimal? GetSeriesValue(List<decimal> values, int shift, int index)
{
var targetIndex = values.Count - 1 - shift - index;
if (targetIndex < 0 || targetIndex >= values.Count)
return null;
return values[targetIndex];
}
private void UpdateOpenTimes(DateTimeOffset openTime)
{
_openTimes.Add(openTime);
while (_openTimes.Count > 4)
_openTimes.RemoveAt(0);
}
private DateTimeOffset? GetOpenTime(int index)
{
var targetIndex = _openTimes.Count - 1 - index;
if (targetIndex < 0 || targetIndex >= _openTimes.Count)
return null;
return _openTimes[targetIndex];
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
DecimalLengthIndicator ma = method switch
{
MovingAverageMethods.Simple => new SMA(),
MovingAverageMethods.Exponential => new EMA(),
MovingAverageMethods.Smoothed => new SmoothedMovingAverage(),
MovingAverageMethods.Weighted => new WeightedMovingAverage(),
_ => new SMA(),
};
ma.Length = Math.Max(1, length);
return ma;
}
}
import clr
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.Strategies import Strategy
from StockSharp.Algo.Indicators import (
SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage
)
class crossing_of_two_ima_strategy(Strategy):
"""Two MA crossover strategy with optional third MA filter, trailing stop and simulated pending orders."""
def __init__(self):
super(crossing_of_two_ima_strategy, self).__init__()
self._first_period = self.Param("FirstMaPeriod", 5) \
.SetGreaterThanZero() \
.SetDisplay("First MA Period", "Period of the first moving average", "First MA")
self._first_shift = self.Param("FirstMaShift", 3) \
.SetDisplay("First MA Shift", "Shift applied to the first MA", "First MA")
self._second_period = self.Param("SecondMaPeriod", 8) \
.SetGreaterThanZero() \
.SetDisplay("Second MA Period", "Period of the second moving average", "Second MA")
self._second_shift = self.Param("SecondMaShift", 5) \
.SetDisplay("Second MA Shift", "Shift applied to the second MA", "Second MA")
self._use_third = self.Param("UseThirdMA", True) \
.SetDisplay("Use Third MA", "Enable third MA as directional filter", "Third MA")
self._third_period = self.Param("ThirdMaPeriod", 13) \
.SetGreaterThanZero() \
.SetDisplay("Third MA Period", "Period of the third moving average", "Third MA")
self._third_shift = self.Param("ThirdMaShift", 8) \
.SetDisplay("Third MA Shift", "Shift applied to the third MA", "Third MA")
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss (pips)", "Initial stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 10) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 4) \
.SetDisplay("Trailing Step (pips)", "Progress before advancing trailing", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Primary candle series", "General")
self._first_values = []
self._second_values = []
self._third_values = []
self._pip_size = 1.0
self._entry_price = None
self._active_sl = None
self._active_tp = None
self._is_long = False
self._first_ma = None
self._second_ma = None
self._third_ma = None
@property
def FirstMaPeriod(self):
return self._first_period.Value
@property
def FirstMaShift(self):
return self._first_shift.Value
@property
def SecondMaPeriod(self):
return self._second_period.Value
@property
def SecondMaShift(self):
return self._second_shift.Value
@property
def UseThirdMA(self):
return self._use_third.Value
@property
def ThirdMaPeriod(self):
return self._third_period.Value
@property
def ThirdMaShift(self):
return self._third_shift.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
@property
def CandleType(self):
return self._candle_type.Value
def _create_ma(self, period):
ma = SimpleMovingAverage()
ma.Length = max(1, period)
return ma
def OnStarted2(self, time):
super(crossing_of_two_ima_strategy, self).OnStarted2(time)
self._first_ma = self._create_ma(self.FirstMaPeriod)
self._second_ma = self._create_ma(self.SecondMaPeriod)
self._third_ma = self._create_ma(self.ThirdMaPeriod) if self.UseThirdMA else None
sec = self.Security
self._pip_size = 1.0
if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0:
self._pip_size = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
self._pip_size *= 10.0
subscription = self.SubscribeCandles(self.CandleType)
if self.UseThirdMA and self._third_ma is not None:
subscription.Bind(self._first_ma, self._second_ma, self._third_ma, self._process_3).Start()
else:
subscription.Bind(self._first_ma, self._second_ma, self._process_2).Start()
def _process_2(self, candle, first_val, second_val):
self._process_internal(candle, first_val, second_val, None)
def _process_3(self, candle, first_val, second_val, third_val):
self._process_internal(candle, first_val, second_val, third_val)
def _process_internal(self, candle, first_val, second_val, third_val):
if candle.State != CandleStates.Finished:
return
self._manage_position(candle)
fv = float(first_val)
sv = float(second_val)
self._update_series(self._first_values, self.FirstMaShift, fv)
self._update_series(self._second_values, self.SecondMaShift, sv)
if self.UseThirdMA and third_val is not None:
self._update_series(self._third_values, self.ThirdMaShift, float(third_val))
if not self._first_ma.IsFormed or not self._second_ma.IsFormed:
return
third_current = None
if self.UseThirdMA:
if self._third_ma is None or not self._third_ma.IsFormed:
return
third_current = self._get_series_val(self._third_values, self.ThirdMaShift, 0)
f0 = self._get_series_val(self._first_values, self.FirstMaShift, 0)
f1 = self._get_series_val(self._first_values, self.FirstMaShift, 1)
s0 = self._get_series_val(self._second_values, self.SecondMaShift, 0)
s1 = self._get_series_val(self._second_values, self.SecondMaShift, 1)
if f0 is None or f1 is None or s0 is None or s1 is None:
return
sl = self.StopLossPips * self._pip_size if self.StopLossPips > 0 else 0.0
tp = self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else 0.0
close = float(candle.ClosePrice)
if f0 > s0 and f1 < s1:
if not self.UseThirdMA or third_current is None or third_current < f0:
self._enter_long(close, sl, tp)
return
if f0 < s0 and f1 > s1:
if not self.UseThirdMA or third_current is None or third_current > f0:
self._enter_short(close, sl, tp)
return
def _enter_long(self, close, sl_offset, tp_offset):
if self.Position > 0:
return
self.BuyMarket()
self._entry_price = close
self._active_sl = close - sl_offset if sl_offset > 0 else None
self._active_tp = close + tp_offset if tp_offset > 0 else None
self._is_long = True
def _enter_short(self, close, sl_offset, tp_offset):
if self.Position < 0:
return
self.SellMarket()
self._entry_price = close
self._active_sl = close + sl_offset if sl_offset > 0 else None
self._active_tp = close - tp_offset if tp_offset > 0 else None
self._is_long = False
def _manage_position(self, candle):
if self.Position == 0:
return
if self._is_long and self.Position > 0:
if self._active_tp is not None and float(candle.HighPrice) >= self._active_tp:
self.SellMarket()
self._reset_position()
return
if self._active_sl is not None and float(candle.LowPrice) <= self._active_sl:
self.SellMarket()
self._reset_position()
return
self._update_trailing_long(candle)
elif not self._is_long and self.Position < 0:
if self._active_tp is not None and float(candle.LowPrice) <= self._active_tp:
self.BuyMarket()
self._reset_position()
return
if self._active_sl is not None and float(candle.HighPrice) >= self._active_sl:
self.BuyMarket()
self._reset_position()
return
self._update_trailing_short(candle)
def _update_trailing_long(self, candle):
if self.TrailingStopPips <= 0:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
target = float(candle.ClosePrice) - trail_dist
if self._active_sl is None or target <= self._active_sl:
return
if trail_step <= 0 or self._active_sl < target - trail_step:
self._active_sl = target
def _update_trailing_short(self, candle):
if self.TrailingStopPips <= 0:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
target = float(candle.ClosePrice) + trail_dist
if self._active_sl is None or target >= self._active_sl:
return
if trail_step <= 0 or self._active_sl > target + trail_step:
self._active_sl = target
def _reset_position(self):
self._entry_price = None
self._active_sl = None
self._active_tp = None
self._is_long = False
def _update_series(self, values, shift, value):
values.append(value)
max_size = max(shift + 3, 3)
while len(values) > max_size:
values.pop(0)
def _get_series_val(self, values, shift, index):
target = len(values) - 1 - shift - index
if target < 0 or target >= len(values):
return None
return values[target]
def OnReseted(self):
super(crossing_of_two_ima_strategy, self).OnReseted()
self._first_values = []
self._second_values = []
self._third_values = []
self._reset_position()
self._first_ma = None
self._second_ma = None
self._third_ma = None
def CreateClone(self):
return crossing_of_two_ima_strategy()