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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the FORTRADER MovingAveragePositionSystem expert advisor.
/// The strategy opens or closes positions on moving average crossings and optionally applies
/// a martingale-like position sizing routine based on cumulative results expressed in MetaTrader points.
/// </summary>
public class MovingAveragePositionSystemStrategy : Strategy
{
/// <summary>
/// Moving average calculation mode.
/// </summary>
public enum MovingAverageModes
{
Simple,
Exponential,
Smoothed,
LinearWeighted,
}
private readonly StrategyParam<MovingAverageModes> _maType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _startVolume;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _lossThresholdPips;
private readonly StrategyParam<decimal> _profitThresholdPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private readonly List<decimal> _maHistory = new();
private decimal _currentVolume;
private decimal _cycleStartRealizedPnL;
private decimal _priceStep;
private decimal _stepPrice;
private decimal _entryPrice;
/// <summary>
/// Moving average type used for signal calculation.
/// </summary>
public MovingAverageModes MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Moving average length.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Forward shift applied to the moving average before generating signals.
/// </summary>
public int MaShift
{
get => _maShift.Value;
set => _maShift.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initial lot size before the martingale routine modifies it.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Base lot size restored after profitable cycles.
/// </summary>
public decimal StartVolume
{
get => _startVolume.Value;
set => _startVolume.Value = value;
}
/// <summary>
/// Maximum allowed lot size.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Loss threshold in MetaTrader points that doubles the next trade volume.
/// </summary>
public decimal LossThresholdPips
{
get => _lossThresholdPips.Value;
set => _lossThresholdPips.Value = value;
}
/// <summary>
/// Profit target in MetaTrader points that resets the volume to the starting lot.
/// </summary>
public decimal ProfitThresholdPips
{
get => _profitThresholdPips.Value;
set => _profitThresholdPips.Value = value;
}
/// <summary>
/// Fixed take profit distance in MetaTrader points.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Enables the martingale-style money management block.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MovingAveragePositionSystemStrategy"/> class.
/// </summary>
public MovingAveragePositionSystemStrategy()
{
_maType = Param(nameof(MaType), MovingAverageModes.LinearWeighted)
.SetDisplay("MA Type", "Moving average method", "Indicators");
_maPeriod = Param(nameof(MaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Moving average length", "Indicators");
_maShift = Param(nameof(MaShift), 0)
.SetRange(0, 100)
.SetDisplay("MA Shift", "Forward shift for the moving average", "Indicators");
_initialVolume = Param(nameof(InitialVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Starting lot size", "Trading");
_startVolume = Param(nameof(StartVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Start Volume", "Base lot restored after profits", "Trading");
_maxVolume = Param(nameof(MaxVolume), 10m)
.SetGreaterThanZero()
.SetDisplay("Max Volume", "Maximum allowed lot size", "Trading");
_lossThresholdPips = Param(nameof(LossThresholdPips), 90m)
.SetGreaterThanZero()
.SetDisplay("Loss Threshold (pts)", "Loss in points that doubles the lot", "Risk");
_profitThresholdPips = Param(nameof(ProfitThresholdPips), 170m)
.SetGreaterThanZero()
.SetDisplay("Profit Target (pts)", "Profit in points that resets the lot", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 1000m)
.SetNotNegative()
.SetDisplay("Take Profit (pts)", "Fixed take profit distance", "Risk");
_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
.SetDisplay("Use Money Management", "Enable martingale volume control", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles used for calculations", "Market Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_maHistory.Clear();
_currentVolume = InitialVolume;
Volume = _currentVolume;
_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
_priceStep = 0m;
_stepPrice = 0m;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_currentVolume = InitialVolume;
Volume = _currentVolume;
_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
_priceStep = Security?.PriceStep ?? 1m;
_stepPrice = _priceStep;
var movingAverage = CreateMovingAverage();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(movingAverage, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, movingAverage);
DrawOwnTrades(area);
}
var takeProfitUnit = TakeProfitPips > 0m ? new Unit(TakeProfitPips, UnitTypes.Absolute) : null;
StartProtection(takeProfitUnit, null);
base.OnStarted2(time);
}
private DecimalLengthIndicator CreateMovingAverage()
{
return MaType switch
{
MovingAverageModes.Exponential => new EMA { Length = MaPeriod },
MovingAverageModes.Smoothed => new SmoothedMovingAverage { Length = MaPeriod },
MovingAverageModes.LinearWeighted => new WeightedMovingAverage { Length = MaPeriod },
_ => new SMA { Length = MaPeriod },
};
}
private void ProcessCandle(ICandleMessage candle, decimal maValue)
{
// Work only with finished candles to reproduce the MQL4 behaviour.
if (candle.State != CandleStates.Finished)
return;
var canTrade = IsFormedAndOnlineAndAllowTrading();
var previousClose = _closeHistory.Count >= 1 ? _closeHistory[^1] : (decimal?)null;
var previousPreviousClose = _closeHistory.Count >= 2 ? _closeHistory[^2] : (decimal?)null;
decimal? shiftedMa = null;
if (_maHistory.Count > MaShift)
{
var index = _maHistory.Count - 1 - MaShift;
if (index >= 0)
shiftedMa = _maHistory[index];
}
if (previousClose.HasValue && previousPreviousClose.HasValue && shiftedMa.HasValue)
{
// Manage existing positions based on the opposite crossing.
ManageOpenPosition(previousClose.Value, shiftedMa.Value);
// Update the working volume according to the martingale routine.
UpdateVolume(previousClose.Value, shiftedMa.Value);
if (canTrade)
{
TryEnter(previousClose.Value, previousPreviousClose.Value, shiftedMa.Value);
}
}
// Store the latest values for the next iteration.
_closeHistory.Add(candle.ClosePrice);
_maHistory.Add(maValue);
}
private void ManageOpenPosition(decimal previousClose, decimal shiftedMa)
{
// Close long positions when the latest closed candle falls back below the moving average.
if (Position > 0 && previousClose < shiftedMa)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
return;
}
// Close short positions when the latest closed candle climbs back above the average.
if (Position < 0 && previousClose > shiftedMa)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
}
}
private void UpdateVolume(decimal previousClose, decimal shiftedMa)
{
if (!UseMoneyManagement)
return;
var realizedPnL = PnLManager?.RealizedPnL ?? 0m;
var realizedDiff = realizedPnL - _cycleStartRealizedPnL;
var stepPrice = _stepPrice != 0m ? _stepPrice : GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;
var priceStep = _priceStep != 0m ? _priceStep : Security?.PriceStep ?? 1m;
var resultInSteps = stepPrice != 0m ? realizedDiff / stepPrice : 0m;
if (Position != 0 && priceStep > 0m && _entryPrice > 0m)
{
// Consider only floating losses as in the original script.
var diff = Position > 0
? previousClose - _entryPrice
: _entryPrice - previousClose;
if (diff < 0m)
{
resultInSteps += diff / priceStep;
}
}
if (resultInSteps <= -LossThresholdPips)
{
// Double the lot size while keeping it within the maximum allowed range.
var newVolume = Math.Min(_currentVolume * 2m, MaxVolume);
if (newVolume > 0m)
{
_currentVolume = newVolume;
NormalizeVolume();
Volume = _currentVolume;
}
if (Position != 0)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
}
_cycleStartRealizedPnL = realizedPnL;
}
else if (resultInSteps >= ProfitThresholdPips)
{
// Reset the lot size to the configured starting volume and lock in profits.
_currentVolume = StartVolume;
NormalizeVolume();
Volume = _currentVolume;
if (Position != 0)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
}
_cycleStartRealizedPnL = realizedPnL;
}
else
{
NormalizeVolume();
}
}
private void TryEnter(decimal previousClose, decimal previousPreviousClose, decimal shiftedMa)
{
NormalizeVolume();
if (_currentVolume <= 0m)
return;
// Detect upward crossing: price moved from below the moving average to above it.
var crossedUp = previousClose > shiftedMa && previousPreviousClose < shiftedMa;
if (crossedUp && Position <= 0)
{
BuyMarket(_currentVolume);
return;
}
// Detect downward crossing: price moved from above the moving average to below it.
var crossedDown = previousClose < shiftedMa && previousPreviousClose > shiftedMa;
if (crossedDown && Position >= 0)
{
SellMarket(_currentVolume);
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position != 0 && _entryPrice == 0m)
_entryPrice = trade.Trade.Price;
if (Position == 0m)
_entryPrice = 0m;
}
private void NormalizeVolume()
{
// Reduce the working lot if it exceeds the maximum allowed size.
while (_currentVolume > MaxVolume && _currentVolume > 0m)
{
_currentVolume /= 2m;
}
if (Portfolio is not null)
{
var portfolioValue = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
var marginThreshold = 1000m * _currentVolume;
while (_currentVolume > 0m && portfolioValue < marginThreshold)
{
_currentVolume /= 2m;
marginThreshold = 1000m * _currentVolume;
}
}
if (_currentVolume < 0m)
{
_currentVolume = 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, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
ExponentialMovingAverage,
SmoothedMovingAverage,
WeightedMovingAverage,
)
class moving_average_position_system_strategy(Strategy):
def __init__(self):
super(moving_average_position_system_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 20) \
.SetDisplay("MA Period", "Moving average length", "Indicators")
self._ma_shift = self.Param("MaShift", 0) \
.SetDisplay("MA Shift", "Forward shift for the moving average", "Indicators")
self._initial_volume = self.Param("InitialVolume", 0.1) \
.SetDisplay("Initial Volume", "Starting lot size", "Trading")
self._start_volume = self.Param("StartVolume", 0.1) \
.SetDisplay("Start Volume", "Base lot restored after profits", "Trading")
self._max_volume = self.Param("MaxVolume", 10.0) \
.SetDisplay("Max Volume", "Maximum allowed lot size", "Trading")
self._loss_threshold_pips = self.Param("LossThresholdPips", 90.0) \
.SetDisplay("Loss Threshold (pts)", "Loss in points that doubles the lot", "Risk")
self._profit_threshold_pips = self.Param("ProfitThresholdPips", 170.0) \
.SetDisplay("Profit Target (pts)", "Profit in points that resets the lot", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 1000.0) \
.SetDisplay("Take Profit (pts)", "Fixed take profit distance", "Risk")
self._use_money_management = self.Param("UseMoneyManagement", True) \
.SetDisplay("Use Money Management", "Enable martingale volume control", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles used for calculations", "Market Data")
self._close_history = []
self._ma_history = []
self._current_volume = 0.1
self._cycle_start_realized_pnl = 0.0
self._price_step = 1.0
self._entry_price = 0.0
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def MaShift(self):
return self._ma_shift.Value
@property
def InitialVolume(self):
return self._initial_volume.Value
@property
def StartVolume(self):
return self._start_volume.Value
@property
def MaxVolume(self):
return self._max_volume.Value
@property
def LossThresholdPips(self):
return self._loss_threshold_pips.Value
@property
def ProfitThresholdPips(self):
return self._profit_threshold_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def UseMoneyManagement(self):
return self._use_money_management.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(moving_average_position_system_strategy, self).OnStarted2(time)
self._current_volume = float(self.InitialVolume)
self.Volume = self._current_volume
ps = self.Security.PriceStep if self.Security is not None else None
self._price_step = float(ps) if ps is not None else 1.0
ma = WeightedMovingAverage()
ma.Length = self.MaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ma, self.ProcessCandle).Start()
tp_pips = float(self.TakeProfitPips)
tp = Unit(tp_pips, UnitTypes.Absolute) if tp_pips > 0 else None
self.StartProtection(tp, None)
def ProcessCandle(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
ma_value = float(ma_value)
can_trade = self.IsFormedAndOnlineAndAllowTrading()
prev_close = self._close_history[-1] if len(self._close_history) >= 1 else None
prev_prev_close = self._close_history[-2] if len(self._close_history) >= 2 else None
shifted_ma = None
ma_shift = self.MaShift
if len(self._ma_history) > ma_shift:
idx = len(self._ma_history) - 1 - ma_shift
if idx >= 0:
shifted_ma = self._ma_history[idx]
if prev_close is not None and prev_prev_close is not None and shifted_ma is not None:
self._manage_open_position(prev_close, shifted_ma)
if can_trade:
self._try_enter(prev_close, prev_prev_close, shifted_ma)
self._close_history.append(float(candle.ClosePrice))
self._ma_history.append(ma_value)
def _manage_open_position(self, prev_close, shifted_ma):
if self.Position > 0 and prev_close < shifted_ma:
self.SellMarket(self.Position)
return
if self.Position < 0 and prev_close > shifted_ma:
self.BuyMarket(abs(self.Position))
def _try_enter(self, prev_close, prev_prev_close, shifted_ma):
if self._current_volume <= 0:
return
crossed_up = prev_close > shifted_ma and prev_prev_close < shifted_ma
if crossed_up and self.Position <= 0:
self.BuyMarket(self._current_volume)
return
crossed_down = prev_close < shifted_ma and prev_prev_close > shifted_ma
if crossed_down and self.Position >= 0:
self.SellMarket(self._current_volume)
def OnOwnTradeReceived(self, trade):
super(moving_average_position_system_strategy, self).OnOwnTradeReceived(trade)
if self.Position != 0 and self._entry_price == 0:
self._entry_price = float(trade.Trade.Price)
if self.Position == 0:
self._entry_price = 0.0
def OnReseted(self):
super(moving_average_position_system_strategy, self).OnReseted()
self._close_history = []
self._ma_history = []
self._current_volume = float(self.InitialVolume)
self._entry_price = 0.0
def CreateClone(self):
return moving_average_position_system_strategy()