GitHub で見る

OSF Countertrend Strategy

The strategy reproduces the Open Source Forex "Overbought/Oversold" countertrend expert. It approximates the original oscillator by averaging several RSI readings and interprets the distance from the equilibrium level (50) as both a direction and position size signal. Trades are executed on finished candles and closed by a fixed take-profit measured in instrument points.

Trading Rules

  • Data: Finished candles of the configured CandleType.
  • Indicator: RSI with period defined by RsiPeriod. The original MQL expert averaged five identical RSI values, therefore a single RSI is sufficient here.
  • Signal logic:
    • When RSI > 50, the market is considered overbought and a short position is opened.
    • When RSI < 50, the market is considered oversold and a long position is opened.
    • The absolute distance |RSI − 50| determines the traded volume through VolumePerPoint.
  • Cooldown: After each trade the strategy waits for CooldownBars finished candles before evaluating a new entry. This mimics the bar smoothing behaviour from the source code.
  • Exits: Each entry places a manual take-profit at TakeProfitPoints * PriceStep away from the fill price. No stop-loss is used, exactly as in the original expert.
  • Reversals: Opening a trade in the opposite direction closes any existing position first by adjusting the market order volume.

Parameters

Parameter Description
RsiPeriod RSI length used to approximate the OSF oscillator (default 14).
VolumePerPoint Volume traded for each RSI point away from the 50 level (default 0.01).
TakeProfitPoints Distance to the take-profit target expressed in instrument points (default 150).
CooldownBars Number of finished candles to skip after each trade (default 5).
CandleType Candle type for indicator calculations (default 1-minute time frame).

Notes

  • The strategy assumes that PriceStep is defined for the selected instrument; otherwise a unit step of 1 is used to compute the take-profit level.
  • Because the original expert had no protective stop-loss, risk management should be added manually when deploying the strategy live.
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>
/// Countertrend strategy based on the Open Source Forex oscillator.
/// </summary>
public class OsfCountertrendStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _volumePerPoint;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private int _cooldown;
	private decimal _longTarget;
	private decimal _shortTarget;

	/// <summary>
	/// RSI period used to approximate the original OSF oscillator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Volume traded per RSI point away from equilibrium (50 level).
	/// </summary>
	public decimal VolumePerPoint
	{
		get => _volumePerPoint.Value;
		set => _volumePerPoint.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in instrument points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Number of finished candles to wait before a new signal can trigger.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="OsfCountertrendStrategy"/>.
	/// </summary>
	public OsfCountertrendStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetRange(2, 200)
		
		.SetDisplay("RSI Period", "RSI length used in oscillator", "General");

		_volumePerPoint = Param(nameof(VolumePerPoint), 0.01m)
		.SetRange(0.001m, 1m)
		
		.SetDisplay("Volume per Point", "Order volume per RSI point from 50", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150m)
		.SetRange(0m, 1000m)
		
		.SetDisplay("Take Profit", "Distance to take profit in points", "Risk");

		_cooldownBars = Param(nameof(CooldownBars), 5)
		.SetRange(0, 50)
		
		.SetDisplay("Cooldown Bars", "Finished candles to wait after trading", "General");

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

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

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

		_rsi = null;
		_cooldown = 0;
		_longTarget = 0m;
		_shortTarget = 0m;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

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

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

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

		if (!_rsi.IsFormed)
		return;

		// indicators already checked above

		// Track active positions for manual take-profit handling.
		if (Position > 0 && _longTarget > 0m && TakeProfitPoints > 0m)
		{
			if (candle.LowPrice <= _longTarget)
			{
				SellMarket();
				_longTarget = 0m;
			}
		}
		else if (Position < 0 && _shortTarget > 0m && TakeProfitPoints > 0m)
		{
			if (candle.HighPrice >= _shortTarget)
			{
				BuyMarket();
				_shortTarget = 0m;
			}
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var diff = rsiValue - 50m;
		if (diff == 0m)
		return;

		var absDiff = Math.Abs(diff);
		var volume = absDiff * VolumePerPoint;
		if (volume <= 0m)
		return;

		var step = Security?.PriceStep ?? 1m;

		if (diff > 0m && Position <= 0m)
		{
			// RSI above 50: countertrend short trade sized by oscillator distance.
			var volumeToSell = volume + Math.Max(0m, Position);
			if (volumeToSell <= 0m)
			return;

			SellMarket();

			_shortTarget = TakeProfitPoints > 0m
			? candle.ClosePrice - step * TakeProfitPoints
			: 0m;
			_longTarget = 0m;
			_cooldown = CooldownBars;
		}
		else if (diff < 0m && Position >= 0m)
		{
			// RSI below 50: countertrend long trade sized by oscillator distance.
			var volumeToBuy = volume + Math.Max(0m, -Position);
			if (volumeToBuy <= 0m)
			return;

			BuyMarket();

			_longTarget = TakeProfitPoints > 0m
			? candle.ClosePrice + step * TakeProfitPoints
			: 0m;
			_shortTarget = 0m;
			_cooldown = CooldownBars;
		}
	}
}