Ver no GitHub

Estratégia The Puncher

Visão geral

  • Convertido do consultor especialista do MetaTrader 5 "The Puncher".
  • Usa um oscilador Estocástico de longo período combinado com RSI para identificar zonas de exaustão.
  • Opera apenas quando a vela atual está fechada, seguindo a abordagem da API de alto nível do StockSharp.
  • Aplica lógica de stop-loss protetor, take-profit, ponto de equilíbrio e stop móvel para gerenciar o risco.

Indicadores

  • Oscilador Estocástico: período base StochasticPeriod, suavização %K StochasticSignalPeriod, suavização %D StochasticSmoothingPeriod.
  • Índice de Força Relativa (RSI): período RsiPeriod.

Parâmetros

Parâmetro Padrão Descrição
StochasticPeriod 100 Período base do oscilador Estocástico.
StochasticSignalPeriod 3 Período de suavização aplicado à linha %K.
StochasticSmoothingPeriod 3 Período de suavização aplicado à linha %D.
RsiPeriod 14 Comprimento de cálculo do RSI.
OversoldLevel 30 Limiar compartilhado pelo Estocástico e RSI para detectar zonas de sobrevenda.
OverboughtLevel 70 Limiar compartilhado pelo Estocástico e RSI para detectar zonas de sobrecompra.
StopLossPips 20 Distância do stop-loss em pips (0 desativa o stop-loss).
TakeProfitPips 50 Distância do take-profit em pips (0 desativa o take-profit).
TrailingStopPips 10 Distância do stop móvel em pips (0 desativa o trailing).
TrailingStepPips 5 Movimento favorável mínimo em pips necessário antes de ajustar o stop móvel novamente.
BreakEvenPips 21 Lucro em pips necessário antes de mover o stop para o ponto de equilíbrio (0 desativa).
CandleType Período de 5 minutos Tipo de vela utilizado para os cálculos.
Volume Propriedade da estratégia Tamanho da ordem utilizado para entradas (configurado via Volume da estratégia).

Tratamento de pips: os parâmetros baseados em pips são convertidos para preços absolutos usando Security.PriceStep. Ajuste Security.PriceStep para o instrumento que você opera.

Regras de trading

Entrada

  • Comprado: quando a linha de sinal do Estocástico e o RSI caem abaixo de OversoldLevel, e não há posição comprada existente.
  • Vendido: quando a linha de sinal do Estocástico e o RSI sobem acima de OverboughtLevel, e não há posição vendida existente.
  • Se um sinal contrário aparecer enquanto há uma posição aberta, a estratégia fecha a posição e aguarda a próxima vela antes de considerar novas entradas.

Saída e gestão de risco

  • Stop-loss: distância fixa definida por StopLossPips.
  • Take-profit: alvo fixo definido por TakeProfitPips.
  • Ponto de equilíbrio: uma vez que o lucro atinge BreakEvenPips, o stop é movido para o preço de entrada.
  • Stop móvel: após o preço se mover favoravelmente em TrailingStopPips, o stop acompanha o mercado e é ajustado a cada TrailingStepPips.
  • Sinais contrários: forçam uma saída mesmo que o stop ou o alvo não tenham sido atingidos.

Notas

  • Funciona com qualquer instrumento suportado pelo StockSharp; os valores padrão são ajustados para pips de câmbio.
  • Usa apenas velas concluídas, reproduzindo o comportamento TradeAtCloseBar=true do robô original.
  • Configure o portfólio, o instrumento e o volume antes de iniciar a estratégia.
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>
/// Stochastic and RSI based strategy converted from "The Puncher".
/// Buys when both oscillators confirm oversold conditions and sells when they confirm overbought conditions.
/// Includes configurable stop-loss, take-profit, break-even and trailing stop logic.
/// </summary>
public class PuncherStrategy : Strategy
{
	private readonly StrategyParam<int> _stochasticPeriod;
	private readonly StrategyParam<int> _stochasticSignalPeriod;
	private readonly StrategyParam<int> _stochasticSmoothingPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _breakEvenPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _breakEvenActivated;
	private decimal? _lastTrailingPrice;

	/// <summary>
	/// Period of the Stochastic oscillator base calculation.
	/// </summary>
	public int StochasticPeriod
	{
		get => _stochasticPeriod.Value;
		set => _stochasticPeriod.Value = value;
	}

	/// <summary>
	/// Period used to smooth the %K line (signal).
	/// </summary>
	public int StochasticSignalPeriod
	{
		get => _stochasticSignalPeriod.Value;
		set => _stochasticSignalPeriod.Value = value;
	}

	/// <summary>
	/// Period used to smooth the %D line.
	/// </summary>
	public int StochasticSmoothingPeriod
	{
		get => _stochasticSmoothingPeriod.Value;
		set => _stochasticSmoothingPeriod.Value = value;
	}

	/// <summary>
	/// RSI calculation period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Oversold threshold shared by Stochastic and RSI.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// Overbought threshold shared by Stochastic and RSI.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips (0 disables the stop-loss).
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips (0 disables the take-profit).
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips (0 disables the trailing stop).
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum price improvement in pips before moving the trailing stop again.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Profit in pips required to move the stop to break-even (0 disables the feature).
	/// </summary>
	public int BreakEvenPips
	{
		get => _breakEvenPips.Value;
		set => _breakEvenPips.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 the strategy.
	/// </summary>
	public PuncherStrategy()
	{
		_stochasticPeriod = Param(nameof(StochasticPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Period", "Base period for the Stochastic oscillator", "Indicators")
			
			.SetOptimize(50, 150, 10);

		_stochasticSignalPeriod = Param(nameof(StochasticSignalPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Signal", "Smoothing period for the %K line", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_stochasticSmoothingPeriod = Param(nameof(StochasticSmoothingPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smoothing", "Smoothing period for the %D line", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "Indicators")
			
			.SetOptimize(7, 28, 1);

		_oversoldLevel = Param(nameof(OversoldLevel), 20m)
			.SetDisplay("Oversold Level", "Threshold for oversold detection", "Signals")
			
			.SetOptimize(10m, 40m, 5m);

		_overboughtLevel = Param(nameof(OverboughtLevel), 80m)
			.SetDisplay("Overbought Level", "Threshold for overbought detection", "Signals")
			
			.SetOptimize(60m, 90m, 5m);

		_stopLossPips = Param(nameof(StopLossPips), 20)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Distance of the protective stop-loss", "Risk")
			
			.SetOptimize(0, 60, 5);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Distance of the profit target", "Risk")
			
			.SetOptimize(0, 120, 10);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
			
			.SetOptimize(0, 40, 5);

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Minimum improvement before trailing stop updates", "Risk")
			
			.SetOptimize(0, 20, 2);

		_breakEvenPips = Param(nameof(BreakEvenPips), 21)
			.SetNotNegative()
			.SetDisplay("Break-Even (pips)", "Profit needed to move the stop to entry", "Risk")
			
			.SetOptimize(0, 40, 2);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for processing", "General");
	}

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

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

		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
		_breakEvenActivated = false;
		_lastTrailingPrice = null;
	}

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

		var 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 rsi)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (ManagePosition(candle))
			return;

		var isBuySignal = rsi < OversoldLevel;
		var isSellSignal = rsi > OverboughtLevel;

		if (Position > 0 && isSellSignal)
		{
			CloseLong();
			return;
		}

		if (Position < 0 && isBuySignal)
		{
			CloseShort();
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (isBuySignal && Position <= 0)
		{
			EnterLong(candle);
			return;
		}

		if (isSellSignal && Position >= 0)
		{
			EnterShort(candle);
		}
	}

	private bool ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			return HandleLongPosition(candle);
		}

		if (Position < 0)
		{
			return HandleShortPosition(candle);
		}

		if (_stopPrice.HasValue || _takeProfitPrice.HasValue || _entryPrice != 0m)
		{
			ResetProtectionState();
		}

		return false;
	}

	private bool HandleLongPosition(ICandleMessage candle)
	{
		if (_entryPrice == 0m)
			_entryPrice = candle.ClosePrice;

		var priceStep = GetPriceStep();

		if (BreakEvenPips > 0 && !_breakEvenActivated)
		{
			var breakEvenPrice = _entryPrice + GetPipValue(BreakEvenPips, priceStep);
			if (candle.HighPrice >= breakEvenPrice)
			{
				if (_stopPrice is null || _stopPrice < _entryPrice)
				{
					_stopPrice = _entryPrice;
					_breakEvenActivated = true;
				}
			}
		}

		if (TrailingStopPips > 0)
		{
			var trailingDistance = GetPipValue(TrailingStopPips, priceStep);
			var trailingStep = TrailingStepPips > 0 ? GetPipValue(TrailingStepPips, priceStep) : 0m;
			_lastTrailingPrice ??= _entryPrice;

			if (candle.HighPrice >= _entryPrice + trailingDistance)
			{
				var referencePrice = _lastTrailingPrice.Value;
				var shouldUpdate = referencePrice == _entryPrice || trailingStep == 0m || candle.HighPrice - referencePrice >= trailingStep;
				if (shouldUpdate)
				{
					var newStop = candle.HighPrice - trailingDistance;
					if (_stopPrice is null || newStop > _stopPrice)
						_stopPrice = newStop;
					_lastTrailingPrice = candle.HighPrice;
				}
			}
		}

		if (_takeProfitPrice is decimal tp && candle.HighPrice >= tp)
		{
			CloseLong();
			return true;
		}

		if (_stopPrice is decimal sl && candle.LowPrice <= sl)
		{
			CloseLong();
			return true;
		}

		return false;
	}

	private bool HandleShortPosition(ICandleMessage candle)
	{
		if (_entryPrice == 0m)
			_entryPrice = candle.ClosePrice;

		var priceStep = GetPriceStep();

		if (BreakEvenPips > 0 && !_breakEvenActivated)
		{
			var breakEvenPrice = _entryPrice - GetPipValue(BreakEvenPips, priceStep);
			if (candle.LowPrice <= breakEvenPrice)
			{
				if (_stopPrice is null || _stopPrice > _entryPrice)
				{
					_stopPrice = _entryPrice;
					_breakEvenActivated = true;
				}
			}
		}

		if (TrailingStopPips > 0)
		{
			var trailingDistance = GetPipValue(TrailingStopPips, priceStep);
			var trailingStep = TrailingStepPips > 0 ? GetPipValue(TrailingStepPips, priceStep) : 0m;
			_lastTrailingPrice ??= _entryPrice;

			if (candle.LowPrice <= _entryPrice - trailingDistance)
			{
				var referencePrice = _lastTrailingPrice.Value;
				var shouldUpdate = referencePrice == _entryPrice || trailingStep == 0m || referencePrice - candle.LowPrice >= trailingStep;
				if (shouldUpdate)
				{
					var newStop = candle.LowPrice + trailingDistance;
					if (_stopPrice is null || newStop < _stopPrice)
						_stopPrice = newStop;
					_lastTrailingPrice = candle.LowPrice;
				}
			}
		}

		if (_takeProfitPrice is decimal tp && candle.LowPrice <= tp)
		{
			CloseShort();
			return true;
		}

		if (_stopPrice is decimal sl && candle.HighPrice >= sl)
		{
			CloseShort();
			return true;
		}

		return false;
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = Volume + (Position < 0 ? -Position : 0m);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_entryPrice = candle.ClosePrice;
		InitializeProtection(isLong: true);
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = Volume + (Position > 0 ? Position : 0m);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_entryPrice = candle.ClosePrice;
		InitializeProtection(isLong: false);
	}

	private void CloseLong()
	{
		if (Position > 0)
			SellMarket(Position);
		ResetProtectionState();
	}

	private void CloseShort()
	{
		if (Position < 0)
			BuyMarket(-Position);
		ResetProtectionState();
	}

	private void InitializeProtection(bool isLong)
	{
		var priceStep = GetPriceStep();
		var stopOffset = StopLossPips > 0 ? GetPipValue(StopLossPips, priceStep) : (decimal?)null;
		var takeOffset = TakeProfitPips > 0 ? GetPipValue(TakeProfitPips, priceStep) : (decimal?)null;

		_stopPrice = isLong
			? (stopOffset.HasValue ? _entryPrice - stopOffset.Value : null)
			: (stopOffset.HasValue ? _entryPrice + stopOffset.Value : null);

		_takeProfitPrice = isLong
			? (takeOffset.HasValue ? _entryPrice + takeOffset.Value : null)
			: (takeOffset.HasValue ? _entryPrice - takeOffset.Value : null);

		_breakEvenActivated = false;
		_lastTrailingPrice = _entryPrice;
	}

	private void ResetProtectionState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
		_breakEvenActivated = false;
		_lastTrailingPrice = null;
	}

	private static decimal GetPipValue(int pips, decimal priceStep)
	{
		return priceStep * pips;
	}

	private decimal GetPriceStep()
	{
		return Security?.PriceStep ?? 1m;
	}
}