在 GitHub 上查看

Puncher 策略

概览

  • 将 MetaTrader 5 智能交易系统 “The Puncher” 转换为 StockSharp 策略。
  • 使用长周期随机指标和 RSI 识别市场超买/超卖区域。
  • 仅在当前蜡烛收盘后执行交易,符合 StockSharp 高阶 API 的工作方式。
  • 通过止损、止盈、保本和跟踪止损综合管理风险。

指标

  • 随机指标:基础周期 StochasticPeriod,%K 平滑周期 StochasticSignalPeriod,%D 平滑周期 StochasticSmoothingPeriod
  • RSI:周期 RsiPeriod

参数

参数 默认值 说明
StochasticPeriod 100 随机指标的基础周期。
StochasticSignalPeriod 3 %K 线的平滑周期。
StochasticSmoothingPeriod 3 %D 线的平滑周期。
RsiPeriod 14 RSI 的计算长度。
OversoldLevel 30 用于判断超卖的阈值(随机指标与 RSI 共用)。
OverboughtLevel 70 用于判断超买的阈值。
StopLossPips 20 止损距离(点),0 表示关闭止损。
TakeProfitPips 50 止盈距离(点),0 表示关闭止盈。
TrailingStopPips 10 跟踪止损距离(点),0 表示关闭。
TrailingStepPips 5 每次收紧跟踪止损所需的最小盈利幅度。
BreakEvenPips 21 盈利达到该点数后将止损移动到入场价(0 表示关闭)。
CandleType 5 分钟周期 用于计算的蜡烛类型。
Volume 策略属性 下单手数,通过策略的 Volume 属性设置。

点值处理:策略使用 Security.PriceStep 将点数转换为绝对价格,请确保品种的最小价格步长设置正确。

交易规则

入场

  • 做多:随机指标的信号线和 RSI 同时低于 OversoldLevel,且当前没有多头仓位。
  • 做空:随机指标的信号线和 RSI 同时高于 OverboughtLevel,且当前没有空头仓位。
  • 若出现反向信号,策略会立即平掉已有仓位,并在下一根蜡烛之前不再开新单。

离场与风控

  • 止损:按照 StopLossPips 设定的固定距离触发。
  • 止盈:按照 TakeProfitPips 设定的固定目标离场。
  • 保本:盈利达到 BreakEvenPips 后,止损移动到入场价。
  • 跟踪止损:价格向有利方向移动 TrailingStopPips 后启动,并每当盈利增加 TrailingStepPips 时收紧止损。
  • 反向信号:即使未触及止损或止盈也会平仓,以保持策略顺势。

备注

  • 适用于任意支持的品种,默认参数针对外汇点值设计。
  • 仅处理已完成的蜡烛,与原始策略 TradeAtCloseBar=true 的行为一致。
  • 启动前请先配置好投资组合、交易标的及下单手数。
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;
	}
}