Ver en GitHub

Daily STP Entry Frame Strategy (StockSharp)

Overview

The Daily STP Entry Frame Strategy replicates the behaviour of the original MetaTrader expert advisor "Daily STP Entry Frame" using the StockSharp high level API. The system prepares breakout stop orders at the start of each new trading day. Entry prices are derived from the previous day's high and low, with additional filters to ensure the market is positioned near these extremes before arming orders. The logic is tailored for Forex-style instruments where "base points" correspond to one tenth of a pip for five-digit quotes.

Core Workflow

  1. Daily range tracking – the strategy subscribes to daily candles to remember the previous session's high and low.
  2. Real-time monitoring – Level1 data supplies the current bid, ask, and last trade prices for intraday management.
  3. Order arming – at the beginning of a new day, if the last price sits at least ThresholdPoints away from yesterday's high/low and the current day's open is on the correct side of that extreme, a stop order is submitted:
    • Buy stop at High + SpreadPoints / 2 (converted to price units).
    • Sell stop at Low - SpreadPoints / 2.
  4. Risk validation – new orders are blocked whenever equity drawdown exceeds MaximumDrawdownPercent or the time filters disallow trading (weekends, hour filter, or day filter).
  5. Position management – once a trade is active the strategy enforces:
    • Static stop-loss and take-profit distances.
    • Optional time-based exit after CloseAfterSeconds.
    • Optional trailing stop emulating the original "SL slope" parameter.
  6. End-of-day hygiene – pending orders are cancelled after NoNewOrdersHour (or the dedicated Friday cutoff) and whenever the calendar day changes.

Trading Rules

  • Long entries
    • Allowed when SideFilter is 0 (both) or 1 (long only).
    • Previous day's high minus current price ≥ ThresholdPoints.
    • Today's opening price is below yesterday's high.
    • Calculated entry price must respect the minimum distance from the current ask.
  • Short entries
    • Allowed when SideFilter is 0 (both) or -1 (short only).
    • Current price minus previous day's low ≥ ThresholdPoints.
    • Today's opening price is above yesterday's low.
    • Calculated entry price must respect the minimum distance from the current bid.
  • Money management
    • Dynamic volume sizing uses a percentage of accumulated profit (PercentOfProfit).
    • Resulting size is bounded by MinVolume and MaxVolume and aligned with the instrument's VolumeStep.
    • Trading pauses automatically once the measured drawdown breaches MaximumDrawdownPercent.
  • Protection logic
    • Stop-loss and take-profit levels are expressed in base points and converted to price offsets using the instrument's pip size.
    • Trailing stop is active only when TrailingSlope < 1. It shifts the protective threshold closer to price as unrealised profit grows.
    • Lifetime exits close any open position once the configured number of seconds elapses.

Parameters

Name Description
CandleType Time-frame used to fetch the reference candles (daily by default).
StopLossPoints Stop-loss distance in base points.
TakeProfitPoints Take-profit distance in base points.
TrailingSlope Portion of profit retained during trailing; ≥ 1 disables the feature.
SideFilter -1 short only, 0 both directions, 1 long only.
ThresholdPoints Minimum gap between current price and the previous extreme required to arm a stop.
SpreadPoints Additional offset (half used above/below the extreme) to compensate for spread.
SlippagePoints Safety buffer added to the minimum stop distance check.
NoNewOrdersHour Hour (platform time) to cancel pending orders on regular days.
NoNewOrdersHourFriday Friday-specific cancellation hour.
EarliestOrderHour Earliest hour of the day when new orders can be created.
DayFilter 6 for all days or 0-5 to trade Sunday through Friday only.
CloseAfterSeconds Optional time-based exit (0 disables).
PercentOfProfit Fraction of accumulated profit used to scale the position size.
MinVolume / MaxVolume Hard bounds for the calculated volume.
MaximumDrawdownPercent Drawdown threshold that blocks new orders.

Conversion Notes

  • Pip conversion mirrors the MetaTrader implementation: if the security exposes 3 or 5 decimal places the base point becomes PriceStep * 10.
  • The stop-order cancellation window reproduces the expert's evening cleanup, including the separate Friday cutoff.
  • Trailing logic follows the original slope formula (newStop = Bid - StopLoss - Slope * (Bid - Entry) for longs).
  • Equity notifications from the MQL version are replaced with strategy log messages.
  • The StockSharp implementation keeps pending orders active even when a position is open, matching the source behaviour.

Usage Tips

  • Assign a Forex instrument with properly configured PriceStep, StepPrice, and VolumeStep values to ensure accurate sizing.
  • Combine the strategy with StockSharp risk controls (portfolio limits, connector-level protections) when running live.
  • Optimise ThresholdPoints, TrailingSlope, and PercentOfProfit using Designer or Runner to adapt the breakout sensitivity to specific symbols.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Daily breakout strategy that enters on price crossing previous session extremes.
/// Converted from "Daily STP Entry Frame" MetaTrader expert.
/// Uses market orders when price breaks above previous high or below previous low.
/// </summary>
public class DailyStpEntryFrameStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;

	private decimal _pipSize;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;

	private decimal? _previousDayHigh;
	private decimal? _previousDayLow;
	private decimal _currentDayHigh;
	private decimal _currentDayLow;
	private DateTime? _currentTradingDay;
	private bool _tradedToday;

	private decimal _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public DailyStpEntryFrameStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time-frame for monitoring", "General");

		_stopLossPoints = Param(nameof(StopLossPoints), 80m)
			.SetNotNegative()
			.SetDisplay("Stop-Loss (points)", "Stop-loss distance", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
			.SetNotNegative()
			.SetDisplay("Take-Profit (points)", "Take-profit distance", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_pipSize = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_previousDayHigh = null;
		_previousDayLow = null;
		_currentDayHigh = 0m;
		_currentDayLow = 0m;
		_currentTradingDay = null;
		_tradedToday = false;
		_entryPrice = 0m;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = Security?.PriceStep ?? 0.01m;
		if (_pipSize <= 0) _pipSize = 0.01m;

		_stopLossOffset = StopLossPoints * _pipSize;
		_takeProfitOffset = TakeProfitPoints * _pipSize;

		_previousDayHigh = null;
		_previousDayLow = null;
		_currentTradingDay = null;
		_tradedToday = false;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var date = candle.OpenTime.Date;

		// Track daily high/low
		if (_currentTradingDay != date)
		{
			// Save previous day's range
			if (_currentTradingDay != null)
			{
				_previousDayHigh = _currentDayHigh;
				_previousDayLow = _currentDayLow;
			}

			_currentTradingDay = date;
			_currentDayHigh = candle.HighPrice;
			_currentDayLow = candle.LowPrice;
			_tradedToday = false;
		}
		else
		{
			_currentDayHigh = Math.Max(_currentDayHigh, candle.HighPrice);
			_currentDayLow = Math.Min(_currentDayLow, candle.LowPrice);
		}

		// Manage existing position
		ManagePosition(candle);

		// Check for breakout entries
		if (_previousDayHigh is null || _previousDayLow is null)
			return;

		if (_tradedToday || Position != 0)
			return;

		var close = candle.ClosePrice;

		// Breakout above previous day high => buy
		if (close > _previousDayHigh.Value)
		{
			_entryPrice = close;
			_longStop = _stopLossOffset > 0 ? close - _stopLossOffset : null;
			_longTake = _takeProfitOffset > 0 ? close + _takeProfitOffset : null;
			_shortStop = null;
			_shortTake = null;
			BuyMarket();
			_tradedToday = true;
		}
		// Breakout below previous day low => sell
		else if (close < _previousDayLow.Value)
		{
			_entryPrice = close;
			_shortStop = _stopLossOffset > 0 ? close + _stopLossOffset : null;
			_shortTake = _takeProfitOffset > 0 ? close - _takeProfitOffset : null;
			_longStop = null;
			_longTake = null;
			SellMarket();
			_tradedToday = true;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				_longStop = null;
				_longTake = null;
				return;
			}
			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				_longStop = null;
				_longTake = null;
			}
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				_shortStop = null;
				_shortTake = null;
				return;
			}
			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				_shortStop = null;
				_shortTake = null;
			}
		}
	}
}