GitHub で見る

Statistics Repeating Behavior Strategy

Intraday strategy that studies how candles behaved at the same time of day during the last N trading sessions. For every new bar it compares the accumulated bullish and bearish body sizes from previous days. If bullish pressure dominates it opens a long position at the bar open, otherwise it goes short. Positions are closed on the next bar and a fixed pip stop loss mimics the original MetaTrader logic. Position size follows a golden-ratio martingale by growing after losses and resetting after wins.

Trading Logic

  1. At the start of each new candle, close any open position from the previous bar.
  2. Look up candles from the last HistoryDays trading days that opened at the same hour and minute.
  3. Sum the candle bodies (in points) separately for bullish and bearish closes, ignoring bodies smaller than MinimumBodyPoints.
  4. If bullish sum exceeds bearish sum → open a long position using the current volume.
  5. If bearish sum exceeds bullish sum → open a short position.
  6. Apply a stop loss of StopLossPips converted through the instrument price step. The stop is checked against intrabar extremes when the candle finishes.
  7. When the trade closes:
    • If the result is profitable, reset volume back to InitialVolume.
    • Otherwise multiply the current volume by MartingaleFactor (respecting volume step and limits).

Parameters

  • HistoryDays (default: 10) — number of previous days to include in the statistics.
  • MinimumBodyPoints (default: 10) — candles with a body smaller than this threshold (in points) are ignored.
  • StopLossPips (default: 15) — pip distance of the protective stop.
  • InitialVolume (default: 0.1) — starting order size before martingale adjustments.
  • MartingaleFactor (default: 1.618) — multiplier applied after a losing trade.
  • CandleType (default: 1 hour) — timeframe used for candles.

Trading Characteristics

  • Market Side: Both long and short depending on statistics.
  • Timeframe: Configurable (default hourly) with exact matching by hour and minute.
  • Position Management: Single position at a time, closed on the next bar or when stop loss is hit.
  • Risk: Uses fixed pip stop and martingale sizing, which can grow volume quickly after consecutive losses.
  • Instruments: Works with instruments that provide a valid MinPriceStep and volume limits.

Implementation Notes

  • Candle bodies are stored per minute-of-day in a rolling queue capped by HistoryDays.
  • Volumes are normalized to the instrument volume step and bounded by MinVolume/MaxVolume.
  • Stop loss detection relies on completed candle extremes to emulate intrabar execution from the original MQL5 expert.
  • All inline code comments are provided in English to align with repository requirements.
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 analyzes historical candle bodies for the same time of day.
/// Opens a position when bullish or bearish pressure dominates over recent days.
/// Implements simple martingale sizing after losing trades.
/// </summary>
public class StatisticsRepeatingBehaviorStrategy : Strategy
{
	private readonly StrategyParam<int> _historyDays;
	private readonly StrategyParam<int> _minimumBodyPoints;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _martingaleFactor;
	private readonly StrategyParam<DataType> _candleType;

	private readonly System.Collections.Concurrent.ConcurrentDictionary<int, BodyStatistics> _bodyStatistics = new();

	private decimal _currentVolume;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private int _positionDirection;
	private decimal _priceStep;
	private TimeSpan _timeFrame;

	/// <summary>
	/// Number of historical days to aggregate for statistics.
	/// </summary>
	public int HistoryDays
	{
		get => _historyDays.Value;
		set => _historyDays.Value = value;
	}

	/// <summary>
	/// Minimum body size in points for a candle to contribute into the statistics.
	/// </summary>
	public int MinimumBodyPoints
	{
		get => _minimumBodyPoints.Value;
		set => _minimumBodyPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initial order size used before applying martingale adjustments.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the order size after a losing trade.
	/// </summary>
	public decimal MartingaleFactor
	{
		get => _martingaleFactor.Value;
		set => _martingaleFactor.Value = value;
	}

	/// <summary>
	/// Candle type to analyze.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="StatisticsRepeatingBehaviorStrategy"/>.
	/// </summary>
	public StatisticsRepeatingBehaviorStrategy()
	{
		_historyDays = Param(nameof(HistoryDays), 3)
			.SetGreaterThanZero()
			.SetDisplay("History Days", "Number of days to collect statistics", "Parameters")
			;

		_minimumBodyPoints = Param(nameof(MinimumBodyPoints), 0)
			.SetDisplay("Minimum Body (points)", "Ignore candles with smaller body", "Parameters")
			;

		_stopLossPips = Param(nameof(StopLossPips), 15)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			;

		_initialVolume = Param(nameof(InitialVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Starting order size", "Trading")
			;

		_martingaleFactor = Param(nameof(MartingaleFactor), 1.618m)
			.SetGreaterThanZero()
			.SetDisplay("Martingale Factor", "Multiplier after losing trade", "Trading")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_bodyStatistics.Clear();
		_currentVolume = 0m;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_positionDirection = 0;
		_priceStep = 0m;
		_timeFrame = TimeSpan.Zero;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_priceStep = Security.PriceStep ?? 1m;

		_timeFrame = CandleType.Arg is TimeSpan span ? span : TimeSpan.Zero;
		if (_timeFrame <= TimeSpan.Zero)
			_timeFrame = TimeSpan.FromMinutes(1);

		_currentVolume = AdjustVolume(InitialVolume);

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		var nextOpen = candle.OpenTime + _timeFrame;
		var nextKey = GetMinuteKey(nextOpen);

		// Close existing position at the beginning of the new bar.
		if (_positionDirection != 0)
		{
			var exitPrice = candle.ClosePrice;
			var stopHit = false;

			if (_positionDirection > 0)
			{
				if (candle.LowPrice <= _stopPrice)
				{
					exitPrice = _stopPrice;
					stopHit = true;
				}
			}
			else
			{
				if (candle.HighPrice >= _stopPrice)
				{
					exitPrice = _stopPrice;
					stopHit = true;
				}
			}

			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();

			UpdateVolumeAfterTrade(exitPrice, stopHit);
		}

		if (_positionDirection == 0 && _bodyStatistics.TryGetValue(nextKey, out var stats) && stats.Count > 0)
		{
			var bullSum = stats.BullSum;
			var bearSum = stats.BearSum;

			if (bullSum > bearSum && Position <= 0)
			{
				EnterPosition(candle, true);
			}
			else if (bearSum > bullSum && Position >= 0)
			{
				EnterPosition(candle, false);
			}
		}

		UpdateStatistics(candle);
	}

	private void EnterPosition(ICandleMessage candle, bool isLong)
	{
		var volume = _currentVolume;
		if (volume <= 0m)
			return;

		var stopDistance = StopLossPips * _priceStep;
		if (stopDistance <= 0m)
			stopDistance = _priceStep;

		if (isLong)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = _entryPrice - stopDistance;
			_positionDirection = 1;
		}
		else
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = _entryPrice + stopDistance;
			_positionDirection = -1;
		}
	}

	private void UpdateVolumeAfterTrade(decimal exitPrice, bool stopHit)
	{
		if (_positionDirection == 0)
			return;

		var profit = (_positionDirection > 0 ? exitPrice - _entryPrice : _entryPrice - exitPrice);

		if (profit > 0m && !stopHit)
		{
			_currentVolume = AdjustVolume(InitialVolume);
		}
		else
		{
			var increased = AdjustVolume(InitialVolume * MartingaleFactor);
			_currentVolume = increased;
		}

		_entryPrice = 0m;
		_stopPrice = 0m;
		_positionDirection = 0;
	}

	private void UpdateStatistics(ICandleMessage candle)
	{
		var currentKey = GetMinuteKey(candle.OpenTime);
		var stats = _bodyStatistics.GetOrAdd(currentKey, _ => new BodyStatistics());

		var body = candle.ClosePrice - candle.OpenPrice;
		var bodyPoints = body / _priceStep;
		var absBody = Math.Abs(bodyPoints);

		if (MinimumBodyPoints > 0 && absBody < MinimumBodyPoints)
			return;

		stats.Enqueue(bodyPoints);

		while (stats.Count > HistoryDays)
		{
			var removed = stats.Dequeue();
			if (removed > 0m)
				stats.BullSum -= removed;
			else if (removed < 0m)
				stats.BearSum -= Math.Abs(removed);
		}
	}

	private decimal AdjustVolume(decimal volume)
	{
		return volume <= 0m ? 1m : volume;
	}

	private static int GetMinuteKey(DateTimeOffset time)
	{
		return time.Hour * 60 + time.Minute;
	}

	private sealed class BodyStatistics
	{
		private readonly Queue<decimal> _values = new();

		public decimal BullSum { get; set; }
		public decimal BearSum { get; set; }

		public int Count => _values.Count;

		public void Enqueue(decimal value)
		{
			_values.Enqueue(value);
			if (value > 0m)
				BullSum += value;
			else if (value < 0m)
				BearSum += Math.Abs(value);
		}

		public decimal Dequeue()
		{
			return _values.Dequeue();
		}
	}
}