Virtual Profit Close replicates the behaviour of the MetaTrader 4 expert advisor Virtual_Profit_Close.mq4. The strategy watches the
current position of the configured security and exits as soon as a virtual profit target is reached. Unlike a regular take-profit order,
the exit level is evaluated internally so no profit orders are left in the order book. A configurable trailing stop can move the exit
price closer to market as the trade moves into profit. When running in testing mode the strategy can automatically open sample positions
to demonstrate its logic.
Conversion Notes
Tick events are consumed through SubscribeTrades().Bind(ProcessTrade).Start() to mimic the original OnTick routine.
MetaTrader "points" are converted to pips by inspecting Security.PriceStep and adjusting for 3/5 digit symbols.
Virtual profit and trailing calculations use the current bid for long positions and the ask for short positions, matching the MQL
implementation that relied on Bid and Ask prices.
The trailing stop logic activates after the configured profit threshold and keeps the stop at a fixed distance from the market
price, similar to repeatedly calling OrderModify in MQL.
A demonstration mode replaces the original strategy tester helper (SendTest) by opening market orders according to the selected
direction and volume. Optional protective stops are placed using SetStopLoss.
Parameters
Parameter
Description
ProfitPips
Virtual take-profit level expressed in MetaTrader pips. The strategy closes the position once the profit exceeds this distance.
UseTrailingStop
Enables trailing behaviour when set to true.
TrailingOffsetPips
Distance maintained between the current price and the trailing stop once it is active.
TrailingActivationPips
Minimum profit in pips required before the trailing stop is engaged.
EnableDemoMode
Automatically opens demonstration orders each time the position becomes flat. Useful for backtests.
DemoOrderDirection
Direction of demo orders (Buy or Sell).
DemoOrderVolume
Volume submitted for demo orders.
DemoStopPips
Optional protective stop for demo orders, expressed in pips.
Behaviour
When the strategy starts it calculates the pip size and distances for profit, trailing and demo stops.
Every tick received through ProcessTrade evaluates the current position:
Long positions are closed when the bid price delivers the configured virtual profit.
Short positions are closed when the ask price covers the same distance in the opposite direction.
If trailing is enabled and the activation threshold is met, the trailing stop moves together with the favourable price movement. Once
the market crosses the trailing level the strategy sends a market order to exit.
Demo mode can automatically open a new position whenever the strategy becomes flat, recreating the tester-only feature of the
original expert.
Requirements
The strategy needs tick-level market data to respond precisely to price changes.
Only one symbol should be assigned to the strategy instance. Multiple simultaneous symbols are not supported, matching the original
MQL implementation that monitored the current chart symbol.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Virtual Profit Close strategy: EMA crossover with profit target management.
/// Enters on EMA crossover, closes when profit target is hit.
/// </summary>
public class VirtualProfitCloseStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private decimal _prevFast;
private decimal _prevSlow;
private bool _hasPrev;
private decimal _entryPrice;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public VirtualProfitCloseStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_fastPeriod = Param(nameof(FastPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicators");
_slowPeriod = Param(nameof(SlowPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicators");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFast = 0m;
_prevSlow = 0m;
_hasPrev = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
_entryPrice = 0;
var fast = new ExponentialMovingAverage { Length = FastPeriod };
var slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fast, slow, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
if (_hasPrev)
{
if (_prevFast <= _prevSlow && fast > slow && Position <= 0)
{
BuyMarket();
_entryPrice = close;
}
else if (_prevFast >= _prevSlow && fast < slow && Position >= 0)
{
SellMarket();
_entryPrice = close;
}
}
_prevFast = fast;
_prevSlow = slow;
_hasPrev = true;
}
}
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.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class virtual_profit_close_strategy(Strategy):
def __init__(self):
super(virtual_profit_close_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 20) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicators")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._entry_price = 0.0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
def OnReseted(self):
super(virtual_profit_close_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._entry_price = 0.0
def OnStarted2(self, time):
super(virtual_profit_close_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
self._has_prev = False
self._entry_price = 0.0
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(15)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
if not self._fast.IsFormed or not self._slow.IsFormed:
return
close = float(candle.ClosePrice)
fast_val = float(fast_value)
slow_val = float(slow_value)
if self._has_prev:
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_fast = fast_val
self._prev_slow = slow_val
self._has_prev = True
def CreateClone(self):
return virtual_profit_close_strategy()