在 GitHub 上查看

Master Mind Triple WPR 策略

概述

  • 移植自 MetaTrader 4 专家顾问 MasterMind3CE(位于 MQL/8458)。
  • 同时使用 26、27、29、30 四个周期的 Williams %R 指标,筛选出极端超买/超卖状态。
  • 属于均值回归思路:在剧烈下跌后买入,在过度上涨后做空。
  • 提供以品种价格跳动(PriceStep)为单位的止损、止盈与可选移动止损设置。
  • 支持任意时间框架,默认处理 15 分钟 K 线。

交易逻辑

指标

  • WilliamsR(26) —— 极短周期振荡指标。
  • WilliamsR(27) —— 较快周期,提供确认信号。
  • WilliamsR(29) —— 中等周期,用于平滑信号。
  • WilliamsR(30) —— 较慢周期,要求多个回溯窗口都处于极值。

四个指标都完成计算后才会参与判断。订阅只处理收盘完结的 K 线,以对应原始顾问 TradeAtCloseBar = true 的行为。

入场条件

  • 多头入场:四个 Williams %R 均小于等于 OversoldLevel(默认 -99.99)。策略目标净头寸为 TradeVolume。若存在空头,会在一次市价单中平掉空单并翻多。
  • 空头入场:四个 Williams %R 均大于等于 OverboughtLevel(默认 -0.01)。策略目标净头寸为 TradeVolume 的空头,若持有多单则先行平仓后翻空。

离场条件

  • 信号反向:当持有多单且出现空头条件(或反之)时,策略会平仓或反手。
  • 止损:可选的价格跳动距离,基于平均入场价设置。若当根 K 线的最高/最低触及该价位,将以市价离场。
  • 止盈:可选的价格跳动目标,到价后平仓。
  • 移动止损:价格在有利方向运行 TrailingStopSteps + TrailingStepSteps 后启动,止损始终跟随在当前收盘价下方/上方 TrailingStopSteps 的距离,且仅当提升幅度不少于 TrailingStepSteps 时才更新。

风险管理

所有距离均以合约的价格跳动(PriceStep)表示。例如 PriceStep = 0.0001StopLossSteps = 2000 时,止损距离为 0.2000。若同方向加仓,策略会按加权方式重新计算平均入场价,保证止损/止盈位置保持一致。当 TrailingStopStepsTrailingStepSteps 中任一为 0 时,移动止损功能关闭。

参数

参数 说明 默认值
TradeVolume 目标净头寸规模(手数或合约数)。 1
OversoldLevel 判断超卖的 Williams %R 阈值。 -99.99
OverboughtLevel 判断超买的 Williams %R 阈值。 -0.01
StopLossSteps 以 PriceStep 表示的止损距离,设为 0 则禁用。 2000
TakeProfitSteps 以 PriceStep 表示的止盈距离,设为 0 则禁用。 0
TrailingStopSteps 以 PriceStep 表示的移动止损距离,需要 TrailingStepSteps > 0 0
TrailingStepSteps 每次更新移动止损所需的最小改进幅度(以 PriceStep 计)。 1
CandleType 策略订阅的 K 线类型/周期。 TimeFrame(15m)

移植说明

  • 原策略中的警报、声音、日志文件与邮件功能被移除,可改用 StockSharp 的日志系统。
  • 原顾问允许在 K 线收盘前下单,本移植版沿用默认的“收盘交易”模式,只在完结 K 线后处理。
  • MetaTrader 专用的魔术号、重复下单、图表对象等功能在 StockSharp 中无对应实现,因此省略。
  • 止损/止盈的调整由策略内部管理,而非持续修改订单;每根 K 线都会重新评估是否触发。

使用建议

  1. 选择需要交易的品种与时间框架,尽量与原策略所使用的图表一致。
  2. 若品种波动特征不同,请调整阈值或风险控制参数。
  3. 启动策略后,它会订阅指定的 K 线,监控 Williams %R 极值并按照规则管理仓位。
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>
/// MasterMind3 port that relies on four Williams %R oscillators hitting extremes.
/// </summary>
public class MasterMindTripleWprStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<int> _stopLossSteps;
	private readonly StrategyParam<int> _takeProfitSteps;
	private readonly StrategyParam<int> _trailingStopSteps;
	private readonly StrategyParam<int> _trailingStepSteps;
	private readonly StrategyParam<DataType> _candleType;

	private WilliamsR _wpr26 = null!;
	private WilliamsR _wpr27 = null!;
	private WilliamsR _wpr29 = null!;
	private WilliamsR _wpr30 = null!;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortTakePrice;

	/// <summary>
	/// Target net position volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Threshold that defines oversold conditions for all oscillators.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// Threshold that defines overbought conditions for all oscillators.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in instrument price steps.
	/// </summary>
	public int StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in instrument price steps.
	/// </summary>
	public int TakeProfitSteps
	{
		get => _takeProfitSteps.Value;
		set => _takeProfitSteps.Value = value;
	}

	/// <summary>
	/// Trailing-stop distance expressed in instrument price steps.
	/// </summary>
	public int TrailingStopSteps
	{
		get => _trailingStopSteps.Value;
		set => _trailingStopSteps.Value = value;
	}

	/// <summary>
	/// Minimal improvement in price steps required before trailing stop is moved.
	/// </summary>
	public int TrailingStepSteps
	{
		get => _trailingStepSteps.Value;
		set => _trailingStepSteps.Value = value;
	}

	/// <summary>
	/// Candle series processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="MasterMindTripleWprStrategy"/>.
	/// </summary>
	public MasterMindTripleWprStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Trade Volume", "Target net position volume", "Trading")
		;

		_oversoldLevel = Param(nameof(OversoldLevel), -99.99m)
		.SetDisplay("Oversold Level", "All Williams %R must be below this level", "Signals")
		;

		_overboughtLevel = Param(nameof(OverboughtLevel), -0.01m)
		.SetDisplay("Overbought Level", "All Williams %R must be above this level", "Signals")
		;

		_stopLossSteps = Param(nameof(StopLossSteps), 2000)
		.SetDisplay("Stop Loss (steps)", "Protective stop distance in price steps", "Risk");

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 0)
		.SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk");

		_trailingStopSteps = Param(nameof(TrailingStopSteps), 0)
		.SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk");

		_trailingStepSteps = Param(nameof(TrailingStepSteps), 1)
		.SetDisplay("Trailing Step (steps)", "Minimal improvement before trailing adjusts", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe to process", "General");
	}

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

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

		_longEntryPrice = null;
		_shortEntryPrice = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_longTakePrice = null;
		_shortTakePrice = null;
	}

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

		Volume = TradeVolume;

		// Initialize Williams %R oscillators with the original periods.
		_wpr26 = new WilliamsR { Length = 26 };
		_wpr27 = new WilliamsR { Length = 27 };
		_wpr29 = new WilliamsR { Length = 29 };
		_wpr30 = new WilliamsR { Length = 30 };

		// Subscribe to candle data and bind all four oscillators.
		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_wpr26, _wpr27, _wpr29, _wpr30, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _wpr26);
			DrawIndicator(area, _wpr27);
			DrawIndicator(area, _wpr29);
			DrawIndicator(area, _wpr30);
			DrawOwnTrades(area);
		}

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal wpr26Value, decimal wpr27Value, decimal wpr29Value, decimal wpr30Value)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (!_wpr26.IsFormed || !_wpr27.IsFormed || !_wpr29.IsFormed || !_wpr30.IsFormed)
		return;

		// Update trailing stops before evaluating exits.
		UpdateTrailingStops(candle);
		TryCloseByRisk(candle);

		var oversoldLevel = OversoldLevel;
		var overboughtLevel = OverboughtLevel;

		var isOversold = wpr26Value <= oversoldLevel &&
		wpr27Value <= oversoldLevel &&
		wpr29Value <= oversoldLevel &&
		wpr30Value <= oversoldLevel;

		var isOverbought = wpr26Value >= overboughtLevel &&
		wpr27Value >= overboughtLevel &&
		wpr29Value >= overboughtLevel &&
		wpr30Value >= overboughtLevel;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (isOversold)
		{
			OpenLong(candle);
		}
		else if (isOverbought)
		{
			OpenShort(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		var target = TradeVolume;
		if (target <= 0m)
		return;

		var current = Position;
		var difference = target - current;
		if (difference <= 0m)
		return;

		var existingLong = Math.Max(current, 0m);

		// A single market order flips the position when needed.
		BuyMarket(difference);

		var entryPrice = candle.ClosePrice;
		UpdateLongState(existingLong, difference, entryPrice);
	}

	private void OpenShort(ICandleMessage candle)
	{
		var target = -TradeVolume;
		if (target >= 0m)
		return;

		var current = Position;
		var difference = current - target;
		if (difference <= 0m)
		return;

		var existingShort = Math.Max(-current, 0m);

		SellMarket(difference);

		var entryPrice = candle.ClosePrice;
		UpdateShortState(existingShort, difference, entryPrice);
	}

	private void TryCloseByRisk(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				// Stop-loss for long positions.
				SellMarket(Position);
				ResetLongState();
				return;
			}

			if (_longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				// Take-profit for long positions.
				SellMarket(Position);
				ResetLongState();
			}
		}
		else if (Position < 0m)
		{
			var shortVolume = Math.Abs(Position);

			if (_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket(shortVolume);
				ResetShortState();
				return;
			}

			if (_shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket(shortVolume);
				ResetShortState();
			}
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (TrailingStopSteps <= 0 || TrailingStepSteps <= 0)
		return;

		var step = GetStepSize();
		var trailingDistance = TrailingStopSteps * step;
		var trailingStep = TrailingStepSteps * step;

		if (Position > 0m && _longEntryPrice.HasValue)
		{
			var profit = candle.ClosePrice - _longEntryPrice.Value;
			if (profit > trailingDistance + trailingStep)
			{
				var newStop = candle.ClosePrice - trailingDistance;

				if (!_longStopPrice.HasValue || newStop > _longStopPrice.Value + trailingStep)
				{
					_longStopPrice = newStop;
				}
			}
		}
		else if (Position < 0m && _shortEntryPrice.HasValue)
		{
			var profit = _shortEntryPrice.Value - candle.ClosePrice;
			if (profit > trailingDistance + trailingStep)
			{
				var newStop = candle.ClosePrice + trailingDistance;

				if (!_shortStopPrice.HasValue || newStop < _shortStopPrice.Value - trailingStep)
				{
					_shortStopPrice = newStop;
				}
			}
		}
	}

	private void UpdateLongState(decimal existingVolume, decimal addedVolume, decimal entryPrice)
	{
		var total = existingVolume + addedVolume;
		if (total <= 0m)
		{
			ResetLongState();
			return;
		}

		if (_longEntryPrice is null || existingVolume <= 0m)
		{
			_longEntryPrice = entryPrice;
		}
		else
		{
			_longEntryPrice = ((_longEntryPrice.Value * existingVolume) + entryPrice * addedVolume) / total;
		}

		var step = GetStepSize();

		if (StopLossSteps > 0)
		{
			_longStopPrice = _longEntryPrice.Value - StopLossSteps * step;
		}
		else if (TrailingStopSteps <= 0)
		{
			_longStopPrice = null;
		}

		_longTakePrice = TakeProfitSteps > 0 ? _longEntryPrice.Value + TakeProfitSteps * step : null;

		ResetShortState();
	}

	private void UpdateShortState(decimal existingVolume, decimal addedVolume, decimal entryPrice)
	{
		var total = existingVolume + addedVolume;
		if (total <= 0m)
		{
			ResetShortState();
			return;
		}

		if (_shortEntryPrice is null || existingVolume <= 0m)
		{
			_shortEntryPrice = entryPrice;
		}
		else
		{
			_shortEntryPrice = ((_shortEntryPrice.Value * existingVolume) + entryPrice * addedVolume) / total;
		}

		var step = GetStepSize();

		if (StopLossSteps > 0)
		{
			_shortStopPrice = _shortEntryPrice.Value + StopLossSteps * step;
		}
		else if (TrailingStopSteps <= 0)
		{
			_shortStopPrice = null;
		}

		_shortTakePrice = TakeProfitSteps > 0 ? _shortEntryPrice.Value - TakeProfitSteps * step : null;

		ResetLongState();
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

	private decimal GetStepSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}