在 GitHub 上查看

Brandy 策略 (C#)

概述

Brandy 策略移植自 MetaTrader 5 的 Brandy (barabashkakvn's edition) 智能交易系统。策略利用两条可配置的移动平均线,通过比较上一根收盘蜡烛的均线位置来判断趋势方向,同时提供以点(pip)为单位的止损、止盈以及移动止损控制。本移植版本基于 StockSharp 的高级策略 API 实现,重现了原始 EA 的交易、风控和参数结构。

策略在开盘价序列上计算一条“快线”均线,在收盘价序列上计算一条“慢线”均线,两条均线的周期、平滑方式、价格源、信号引用柱以及位移参数均可独立设置。若上一根已完成蜡烛的快线与慢线同时位于各自信号值的同一侧,则产生做多或做空信号。策略还会在每根蜡烛收盘时利用开盘价均线进行趋势校验,一旦条件被破坏便立即平仓,从而保持原始 EA 的保护逻辑。所有止损、止盈及移动止损距离均使用 pip 表示,并依据品种的最小报价步长及五位报价的特殊调整转换为绝对价格。

交易流程

  1. 每当生成新的完整蜡烛时,策略使用设定的平滑方法与价格源更新开盘均线与收盘均线,同时保存指定数量的历史值,以模拟 MT5 iMA 函数的信号柱和位移行为。
  2. 当当前没有持仓时:
    • 若上一柱开盘均线值大于配置的信号值(包含位移),且
    • 上一柱收盘均线值也大于其信号参考值(注意:原 EA 在此处与开盘均线比较,本移植保持该特性以保证兼容), 则按固定手数开多。
  3. 若上述两条均线同时低于各自信号值,则开空。
  4. 持仓期间,每根蜡烛收盘后按以下顺序检查离场条件:
    • 趋势反转:若开盘均线跌破信号值(多头)或升破信号值(空头),立即以市价平仓;
    • 移动止损:当浮动利润超过“移动止损距离 + 触发步长”时,将保护价位上调/下调至距离当前收盘价 TrailingStopPips 的位置;
    • 止盈:若本根蜡烛的高/低价触及目标价,则以市价平仓;
    • 止损:若蜡烛范围突破保护价位,同样平仓。
  5. 交易数量固定,由 TradeVolume 参数决定,默认值与 MT5 版本的 0.1 手保持一致。

参数说明

参数 含义
TradeVolume 下单手数(lots)。
StopLossPips 止损距离(pip,0 表示关闭)。
TakeProfitPips 止盈距离(pip,0 表示关闭)。
TrailingStopPips 移动止损基础距离(pip)。需与 TrailingStepPips 配合使用。
TrailingStepPips 更新移动止损前必须额外前进的 pip 数,开启移动止损时必须为正数。
MaClosePeriod / MaOpenPeriod 收盘/开盘均线的周期长度。
MaCloseShift / MaOpenShift 对应均线的位移量(以柱为单位)。
MaCloseSignalBar / MaOpenSignalBar 用于比较的历史柱索引。0 表示最新值,1 表示上一柱,以此类推。
MaCloseMethod / MaOpenMethod 均线平滑方式:SMA、EMA、SMMA、LWMA。
MaCloseAppliedPrice / MaOpenAppliedPrice 均线计算所使用的蜡烛价格源:收盘、开盘、最高、最低、中位、典型、加权。
CandleType 订阅的蜡烛周期。

实现细节

  • Pip 大小基于 Security.PriceStep 计算。当品种小数位数为 3 或 5 时,将步长乘以 10,以复刻 MT5 点与 pip 的换算。
  • 为复现 iMA 的信号柱与位移功能,代码使用受限队列缓存均线历史值,避免调用被禁止的 GetValue() 等指示器接口。
  • 收盘均线的比较对象依旧使用开盘均线缓存,这是源代码中 iMAGet(handle_iMAOpen, MaClose_SignalBar) 的原始行为,移植版本保持该特性以兼容已有参数配置。
  • 止损、止盈及移动止损在蜡烛收盘时执行,逻辑尽可能贴近 EA 在服务器端修改订单的过程,同时遵守 StockSharp 高级 API 的限制。

使用建议

  • 请根据原始策略的周期设定 CandleType,确保均线计算与回测时间框一致。
  • 若不需要移动止损,可将 TrailingStopPips 设为 0;如需启用,必须提供正数的 TrailingStepPips,否则策略会在启动时抛出异常。
  • 在 StockSharp 中测试时,请确认品种的 PriceStepDecimals 与目标市场匹配,以确保 pip 转换后的价格距离准确。
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>
/// Conversion of the Brandy Expert Advisor from MetaTrader 5.
/// Combines two configurable moving averages to generate entries and manages positions with trailing exits.
/// </summary>
public class BrandyStrategy : Strategy
{
	/// <summary>
	/// Supported moving average smoothing methods.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smma,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Lwma
	}

	/// <summary>
	/// Price sources that can be fed into the moving averages.
	/// </summary>
	public enum AppliedPriceTypes
	{
		/// <summary>
		/// Candle close price.
		/// </summary>
		Close,

		/// <summary>
		/// Candle open price.
		/// </summary>
		Open,

		/// <summary>
		/// Candle high price.
		/// </summary>
		High,

		/// <summary>
		/// Candle low price.
		/// </summary>
		Low,

		/// <summary>
		/// Median price of the candle (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (high + low + close) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted price (high + low + 2 * close) / 4.
		/// </summary>
		Weighted
	}
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _maClosePeriod;
	private readonly StrategyParam<int> _maCloseShift;
	private readonly StrategyParam<MovingAverageMethods> _maCloseMethod;
	private readonly StrategyParam<AppliedPriceTypes> _maCloseAppliedPrice;
	private readonly StrategyParam<int> _maCloseSignalBar;
	private readonly StrategyParam<int> _maOpenPeriod;
	private readonly StrategyParam<int> _maOpenShift;
	private readonly StrategyParam<MovingAverageMethods> _maOpenMethod;
	private readonly StrategyParam<AppliedPriceTypes> _maOpenAppliedPrice;
	private readonly StrategyParam<int> _maOpenSignalBar;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _maOpenIndicator;
	private DecimalLengthIndicator _maCloseIndicator;
	private decimal _pipSize;
	private readonly List<decimal> _maOpenValues = [];
	private readonly List<decimal> _maCloseValues = [];
	private int _maxOpenQueueSize;
	private int _maxCloseQueueSize;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Trading volume per order.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

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

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Step that defines how far the price must move before the trailing stop is advanced.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Period of the moving average calculated on the close series.
	/// </summary>
	public int MaClosePeriod
	{
		get => _maClosePeriod.Value;
		set => _maClosePeriod.Value = value;
	}

	/// <summary>
	/// Displacement applied to the moving average calculated on closes.
	/// </summary>
	public int MaCloseShift
	{
		get => _maCloseShift.Value;
		set => _maCloseShift.Value = value;
	}

	/// <summary>
	/// Moving average smoothing method for the close series.
	/// </summary>
	public MovingAverageMethods MaCloseMethod
	{
		get => _maCloseMethod.Value;
		set => _maCloseMethod.Value = value;
	}

	/// <summary>
	/// Price source used by the close moving average.
	/// </summary>
	public AppliedPriceTypes MaCloseAppliedPrice
	{
		get => _maCloseAppliedPrice.Value;
		set => _maCloseAppliedPrice.Value = value;
	}

	/// <summary>
	/// Bar index used as a signal reference for the close moving average.
	/// </summary>
	public int MaCloseSignalBar
	{
		get => _maCloseSignalBar.Value;
		set => _maCloseSignalBar.Value = value;
	}

	/// <summary>
	/// Period of the moving average calculated on the open series.
	/// </summary>
	public int MaOpenPeriod
	{
		get => _maOpenPeriod.Value;
		set => _maOpenPeriod.Value = value;
	}

	/// <summary>
	/// Displacement applied to the moving average calculated on opens.
	/// </summary>
	public int MaOpenShift
	{
		get => _maOpenShift.Value;
		set => _maOpenShift.Value = value;
	}

	/// <summary>
	/// Moving average smoothing method for the open series.
	/// </summary>
	public MovingAverageMethods MaOpenMethod
	{
		get => _maOpenMethod.Value;
		set => _maOpenMethod.Value = value;
	}

	/// <summary>
	/// Price source used by the open moving average.
	/// </summary>
	public AppliedPriceTypes MaOpenAppliedPrice
	{
		get => _maOpenAppliedPrice.Value;
		set => _maOpenAppliedPrice.Value = value;
	}

	/// <summary>
	/// Bar index used as a signal reference for the open moving average.
	/// </summary>
	public int MaOpenSignalBar
	{
		get => _maOpenSignalBar.Value;
		set => _maOpenSignalBar.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="BrandyStrategy"/> class.
	/// </summary>
	public BrandyStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Trade Volume", "Order size in lots", "General");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (pips)", "Distance for trailing stop", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk");

		_maClosePeriod = Param(nameof(MaClosePeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("MA Close Period", "Length of MA calculated on close", "Indicators");

		_maCloseShift = Param(nameof(MaCloseShift), 0)
		.SetNotNegative()
		.SetDisplay("MA Close Shift", "Forward shift applied to close MA", "Indicators");

		_maCloseMethod = Param(nameof(MaCloseMethod), MovingAverageMethods.Ema)
		.SetDisplay("MA Close Method", "Smoothing method for close MA", "Indicators");

		_maCloseAppliedPrice = Param(nameof(MaCloseAppliedPrice), AppliedPriceTypes.Close)
		.SetDisplay("MA Close Price", "Price source for close MA", "Indicators");

		_maCloseSignalBar = Param(nameof(MaCloseSignalBar), 0)
		.SetNotNegative()
		.SetDisplay("MA Close Signal Bar", "Reference bar index for close MA", "Indicators");

		_maOpenPeriod = Param(nameof(MaOpenPeriod), 70)
		.SetGreaterThanZero()
		.SetDisplay("MA Open Period", "Length of MA calculated on open", "Indicators");

		_maOpenShift = Param(nameof(MaOpenShift), 0)
		.SetNotNegative()
		.SetDisplay("MA Open Shift", "Forward shift applied to open MA", "Indicators");

		_maOpenMethod = Param(nameof(MaOpenMethod), MovingAverageMethods.Ema)
		.SetDisplay("MA Open Method", "Smoothing method for open MA", "Indicators");

		_maOpenAppliedPrice = Param(nameof(MaOpenAppliedPrice), AppliedPriceTypes.Close)
		.SetDisplay("MA Open Price", "Price source for open MA", "Indicators");

		_maOpenSignalBar = Param(nameof(MaOpenSignalBar), 0)
		.SetNotNegative()
		.SetDisplay("MA Open Signal Bar", "Reference bar index for open MA", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame of input candles", "General");

		Volume = _tradeVolume.Value;
	}

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

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

		_maOpenIndicator = null;
		_maCloseIndicator = null;
		_maOpenValues.Clear();
		_maCloseValues.Clear();
		_pipSize = 0m;
		_maxOpenQueueSize = 0;
		_maxCloseQueueSize = 0;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		_maOpenIndicator = CreateMovingAverage(MaOpenMethod, MaOpenPeriod);
		_maCloseIndicator = CreateMovingAverage(MaCloseMethod, MaClosePeriod);

		UpdatePipSize();
		UpdateQueueSizes();

		Volume = _tradeVolume.Value;

		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;

		if (_maOpenIndicator == null || _maCloseIndicator == null)
		return;

		var openSource = GetAppliedPrice(candle, MaOpenAppliedPrice);
		var closeSource = GetAppliedPrice(candle, MaCloseAppliedPrice);

		var maOpenResult = _maOpenIndicator!.Process(new DecimalIndicatorValue(_maOpenIndicator, openSource, candle.OpenTime) { IsFinal = true });
		var maCloseResult = _maCloseIndicator!.Process(new DecimalIndicatorValue(_maCloseIndicator, closeSource, candle.OpenTime) { IsFinal = true });

		if (maOpenResult.IsEmpty || maCloseResult.IsEmpty || !_maOpenIndicator.IsFormed || !_maCloseIndicator.IsFormed)
			return;

		var maOpen = maOpenResult.ToDecimal();
		var maClose = maCloseResult.ToDecimal();

		EnqueueValue(_maOpenValues, maOpen, _maxOpenQueueSize);
		EnqueueValue(_maCloseValues, maClose, _maxCloseQueueSize);

		var maOpenPrev = GetQueueValue(_maOpenValues, 1 + MaOpenShift);
		var maOpenSignal = GetQueueValue(_maOpenValues, MaOpenSignalBar + MaOpenShift);
		var maClosePrev = GetQueueValue(_maCloseValues, 1 + MaCloseShift);
		var maCloseSignal = GetQueueValue(_maCloseValues, MaCloseSignalBar + MaCloseShift);

		if (maOpenPrev is null || maOpenSignal is null || maClosePrev is null || maCloseSignal is null)
		return;

		var longSignal = maOpenPrev > maOpenSignal && maClosePrev > maCloseSignal;
		var shortSignal = maOpenPrev < maOpenSignal && maClosePrev < maCloseSignal;

		if (Position == 0)
		{
			if (longSignal)
			{
				OpenLong(candle.ClosePrice);
			}
			else if (shortSignal)
			{
				OpenShort(candle.ClosePrice);
			}
		}
		else
		{
			ManageOpenPosition(candle, maOpenPrev.Value, maOpenSignal.Value);
		}
	}

	private void OpenLong(decimal price)
	{
		var volume = Volume;
		if (volume <= 0m)
		return;

		_entryPrice = price;
		_stopPrice = StopLossPips > 0m ? price - StopLossPips * _pipSize : null;
		_takePrice = TakeProfitPips > 0m ? price + TakeProfitPips * _pipSize : null;

		BuyMarket(volume);
	}

	private void OpenShort(decimal price)
	{
		var volume = Volume;
		if (volume <= 0m)
		return;

		_entryPrice = price;
		_stopPrice = StopLossPips > 0m ? price + StopLossPips * _pipSize : null;
		_takePrice = TakeProfitPips > 0m ? price - TakeProfitPips * _pipSize : null;

		SellMarket(volume);
	}

	private void ManageOpenPosition(ICandleMessage candle, decimal maOpenPrev, decimal maOpenSignal)
	{
		var position = Position;

		if (position > 0)
		{
			if (maOpenPrev < maOpenSignal)
			{
				SellMarket(position);
				ResetPositionState();
				return;
			}

			UpdateTrailingForLong(candle);

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(position);
				ResetPositionState();
				return;
			}

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(position);
				ResetPositionState();
			}
		}
		else if (position < 0)
		{
			if (maOpenPrev > maOpenSignal)
			{
				BuyMarket(-position);
				ResetPositionState();
				return;
			}

			UpdateTrailingForShort(candle);

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(-position);
				ResetPositionState();
				return;
			}

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(-position);
				ResetPositionState();
			}
		}
		else
		{
			ResetPositionState();
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _entryPrice is null)
		return;

		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;

		if (trailingStop <= 0m)
		return;

		var currentPrice = candle.ClosePrice;
		var entryPrice = _entryPrice.Value;

		if (currentPrice - entryPrice <= trailingStop + trailingStep)
		return;

		var threshold = currentPrice - (trailingStop + trailingStep);

		if (_stopPrice.HasValue && _stopPrice.Value >= threshold)
		return;

		var newStop = currentPrice - trailingStop;
		if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _entryPrice is null)
		return;

		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;

		if (trailingStop <= 0m)
		return;

		var currentPrice = candle.ClosePrice;
		var entryPrice = _entryPrice.Value;

		if (entryPrice - currentPrice <= trailingStop + trailingStep)
		return;

		var threshold = currentPrice + trailingStop + trailingStep;

		if (_stopPrice.HasValue && _stopPrice.Value <= threshold)
		return;

		var newStop = currentPrice + trailingStop;
		if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
		_stopPrice = newStop;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void UpdatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		_pipSize = step;
	}

	private void UpdateQueueSizes()
	{
		var shiftOpen = Math.Max(0, MaOpenShift);
		var shiftClose = Math.Max(0, MaCloseShift);
		var openDepth = Math.Max(Math.Max(1 + shiftOpen, MaOpenSignalBar + shiftOpen), MaCloseSignalBar + shiftOpen);
		var closeDepth = Math.Max(1 + shiftClose, 1);

		_maxOpenQueueSize = Math.Max(2, openDepth + 2);
		_maxCloseQueueSize = Math.Max(2, closeDepth + 2);
	}

	private static void EnqueueValue(List<decimal> queue, decimal value, int maxSize)
	{
		queue.Add(value);

		while (queue.Count > maxSize)
			queue.RemoveAt(0);
	}

	private static decimal? GetQueueValue(List<decimal> queue, int indexFromCurrent)
	{
		if (indexFromCurrent < 0)
			return null;

		if (queue.Count <= indexFromCurrent)
			return null;

		var targetIndex = queue.Count - 1 - indexFromCurrent;

		return targetIndex >= 0 && targetIndex < queue.Count
			? queue[targetIndex]
			: null;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		return method switch
		{
		MovingAverageMethods.Sma => new SimpleMovingAverage { Length = length },
		MovingAverageMethods.Ema => new ExponentialMovingAverage { Length = length },
		MovingAverageMethods.Smma => new SmoothedMovingAverage { Length = length },
		MovingAverageMethods.Lwma => new WeightedMovingAverage { Length = length },
		_ => new ExponentialMovingAverage { Length = length }
		};
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPriceTypes priceType)
	{
		return priceType switch
		{
			AppliedPriceTypes.Close => candle.ClosePrice,
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + candle.ClosePrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}