在 GitHub 上查看

MP 烛形策略

概述

MP 烛形策略 是将 MetaTrader 5 专家顾问 mp candlestick.mq5 移植到 StockSharp 高级策略框架的结果。系统基于完成蜡烛的方向选择交易方向,同时执行严格的资金与风险管理。策略既支持以 MetaTrader 点 (pip) 表示的固定止损距离,也支持基于平均真实波幅 (ATR) 的自适应止损方案。

交易逻辑

  1. 订阅一个可配置的蜡烛序列(默认:1 小时蜡烛)。
  2. 当出现新的完结蜡烛时:
    • 收盘价高于开盘价 → 计划做多。
    • 收盘价低于开盘价 → 计划做空。
    • 开收盘相同或几乎相同的十字星将被忽略。
  3. 在入场前根据设定的模式计算止损价位:
    • 若启用 ATR,则使用 ATR × 1.5 作为止损距离。
    • 若禁用 ATR,则使用输入的固定点数转换成价格距离。
  4. 使用设定的风险收益比计算目标价,验证预估保证金占用是否低于阈值,并根据风险金额得出下单手数。
  5. 成交后在后续蜡烛中监控:
    • 通过蜡烛最高价/最低价检测是否触发止损或止盈。
    • 当启用 ATR 风控时,动态上调/下调止损价位以跟随价格。
  6. 仓位平仓后重新等待下一根完结蜡烛以寻找新的机会。

风险与资金管理

  • Risk Percent 控制账户权益中可承受的最大亏损比例,基于止损距离与合约步长计算下单量。
  • Risk/Reward Ratio 将初始风险距离乘以指定倍数,用以确定止盈价位。
  • Max Margin Usage 限制新仓所需的预估保证金占当前权益的百分比,防止过度杠杆。
  • Trailing Stop 在启用 ATR 的情况下自动生效,在不超过最新收盘价的前提下,把止损向盈利方向移动以锁定收益。

参数

参数 默认值 说明
RiskPercent 1 单笔交易允许承担的权益百分比。
RiskRewardRatio 1.5 止盈距离与初始风险距离之间的倍数关系。
MaxMarginUsage 30 估算保证金占用的上限(权益百分比)。
StopLossPips 50 关闭 ATR 时使用的固定止损点数。
UseAutoSl true 是否启用 ATR × 1.5 的自适应止损。
CandleType 1 小时时间框架 信号与 ATR 计算使用的蜡烛序列。

实现要点

  • 使用 StockSharp 的 SubscribeCandlesAverageTrueRange 指示器绑定实现高层数据流。
  • 仓位大小符合交易品种的成交量步长、最小及最大手数约束。
  • 保证金检查优先使用合约提供的 MarginBuy/MarginSell 信息,不可用时退化为价格近似。
  • 通过监控蜡烛极值来触发止损与止盈,保证不同交易所环境的一致性。
  • 代码中的注释全部使用英文,以符合转换指南的要求。

文件列表

  • CS/MpCandlestickStrategy.cs — C# 主策略实现。
  • README.md — 英文文档。
  • README_zh.md — 中文文档(本文件)。
  • README_ru.md — 俄文文档。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Candlestick-based risk managed strategy converted from the MetaTrader "mp candlestick" expert.
/// Uses candle direction to decide trade side, applies ATR-based or fixed stop-loss distance,
/// and enforces a configurable risk-to-reward profile with margin awareness.
/// </summary>
public class MpCandlestickStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _riskRewardRatio;
	private readonly StrategyParam<decimal> _maxMarginUsage;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<bool> _useAutoSl;
	private readonly StrategyParam<DataType> _candleType;

	private AverageTrueRange _atr;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _isLongPosition;

	/// <summary>
	/// Percentage of portfolio equity risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Desired risk to reward ratio.
	/// </summary>
	public decimal RiskRewardRatio
	{
		get => _riskRewardRatio.Value;
		set => _riskRewardRatio.Value = value;
	}

	/// <summary>
	/// Maximum allowed margin usage percentage.
	/// </summary>
	public decimal MaxMarginUsage
	{
		get => _maxMarginUsage.Value;
		set => _maxMarginUsage.Value = value;
	}

	/// <summary>
	/// Fixed stop-loss distance in MetaTrader pips when dynamic stop is disabled.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Enables ATR based stop-loss sizing.
	/// </summary>
	public bool UseAutoSl
	{
		get => _useAutoSl.Value;
		set => _useAutoSl.Value = value;
	}

	/// <summary>
	/// Candle type used for signal generation and ATR calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public MpCandlestickStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
		.SetNotNegative()
		.SetDisplay("Risk Percent", "Percentage of portfolio equity risked per trade", "Risk")
		
		.SetOptimize(0.5m, 10m, 0.5m);

		_riskRewardRatio = Param(nameof(RiskRewardRatio), 1.5m)
		.SetGreaterThanZero()
		.SetDisplay("Risk/Reward Ratio", "Target reward multiple relative to the initial risk", "Risk")
		
		.SetOptimize(1m, 4m, 0.25m);

		_maxMarginUsage = Param(nameof(MaxMarginUsage), 30m)
		.SetNotNegative()
		.SetDisplay("Max Margin Usage", "Upper bound for margin consumption as percent of equity", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50)
		.SetGreaterThanZero()
		.SetDisplay("Stop-Loss Pips", "Fixed stop-loss size in MetaTrader pips", "Risk")
		
		.SetOptimize(10, 200, 5);

		_useAutoSl = Param(nameof(UseAutoSl), true)
		.SetDisplay("Use ATR Stop", "If enabled the stop-loss uses ATR * 1.5 distance", "Risk");

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

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

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

		_atr = null;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}

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

		_atr = new AverageTrueRange { Length = 14 };

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

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

	/// <inheritdoc />
	// Reset risk levels handled via OnReseted

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
			ResetRiskLevels();
	}

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

		CheckRiskLevels(candle);

		// indicators are bound via Bind

		if (Position != 0m)
		{
			UpdateTrailingStop(candle.ClosePrice);
			return;
		}

		var isBullish = candle.ClosePrice > candle.OpenPrice;
		var isBearish = candle.ClosePrice < candle.OpenPrice;

		if (!isBullish && !isBearish)
			return;

		if (!TryCreateRiskTargets(isBullish, candle.ClosePrice, atrValue,
		out var stopPrice, out var takeProfit, out var stopDistance))
		{
			return;
		}

		var volume = CalculateTradeVolume(stopDistance);
		if (volume <= 0m)
			return;

		// margin validation skipped for backtest

		if (isBullish)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_entryPrice = candle.ClosePrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfit;
		_isLongPosition = isBullish;
		UpdateTrailingStop(candle.ClosePrice);
	}

	private bool TryCreateRiskTargets(bool isLong, decimal entryPrice, decimal atrValue,
	out decimal stopPrice, out decimal takeProfitPrice, out decimal stopDistance)
	{
		stopPrice = 0m;
		takeProfitPrice = 0m;
		stopDistance = 0m;

		var security = Security;
		if (security == null)
			return false;

		if (RiskRewardRatio <= 0m)
			return false;

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		decimal distance;
		if (UseAutoSl)
		{
			distance = atrValue * 1.5m;
		}
		else
		{
			distance = StopLossPips * priceStep;
		}

		if (distance <= 0m)
			return false;

		stopDistance = distance;
		stopPrice = isLong ? entryPrice - distance : entryPrice + distance;
		takeProfitPrice = isLong ? entryPrice + distance * RiskRewardRatio : entryPrice - distance * RiskRewardRatio;

		return stopPrice > 0m && takeProfitPrice > 0m;
	}

	private decimal CalculateTradeVolume(decimal stopDistance)
	{
		var security = Security;
		var portfolio = Portfolio;

		if (security == null || portfolio == null)
			return 0m;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

		if (RiskPercent <= 0m)
			return AlignVolume(volumeStep);

		var equity = portfolio.CurrentValue ?? portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return 0m;

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? priceStep;
		if (stepPrice <= 0m)
			stepPrice = priceStep;

		if (stopDistance <= 0m)
			return 0m;

		var steps = stopDistance / priceStep;
		if (steps <= 0m)
			return 0m;

		var lossPerVolumeStep = steps * stepPrice;
		if (lossPerVolumeStep <= 0m)
			return 0m;

		var riskAmount = equity * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		var rawVolume = riskAmount / lossPerVolumeStep * volumeStep;
		return AlignVolume(rawVolume);
	}

	private decimal AlignVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
			return 0m;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

		if (volume <= 0m)
			volume = volumeStep;

		var steps = Math.Floor(volume / volumeStep);
		if (steps <= 0m)
			steps = 1m;

		var normalized = steps * volumeStep;

		var minVolume = security.MinVolume ?? volumeStep;
		if (normalized < minVolume)
			normalized = minVolume;

		var maxVolume = security.MaxVolume;
		if (maxVolume.HasValue && normalized > maxVolume.Value)
			normalized = maxVolume.Value;

		return normalized;
	}

	private bool ValidateMargin(decimal price, decimal volume, bool isLong)
	{
		if (MaxMarginUsage <= 0m)
			return true;

		var security = Security;
		var portfolio = Portfolio;
		if (security == null || portfolio == null)
			return false;

		var equity = portfolio.CurrentValue ?? portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return false;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

		var marginPerVolume = isLong ? GetSecurityValue<decimal?>(Level1Fields.MarginBuy) : GetSecurityValue<decimal?>(Level1Fields.MarginSell);

		decimal margin;
		if (marginPerVolume is decimal direct && direct > 0m)
		{
			margin = direct * (volume / volumeStep);
		}
		else
		{
			margin = price * volume;
		}

		var maxMargin = equity * (MaxMarginUsage / 100m);
		return margin <= maxMargin;
	}

	private void CheckRiskLevels(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket(Position);
				ResetRiskLevels();
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
	}

	private void UpdateTrailingStop(decimal currentPrice)
	{
		if (!UseAutoSl)
			return;

		if (_entryPrice is not decimal entry || _takeProfitPrice is not decimal take || _stopPrice is not decimal currentStop)
			return;

		var security = Security;
		if (security == null)
			return;

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		if (_isLongPosition)
		{
			var candidate = entry + (take - entry) * 0.5m;
			var limit = currentPrice - priceStep;
			if (limit <= entry)
				limit = entry;

			if (candidate > limit)
				candidate = limit;

			if (candidate > currentStop && candidate < currentPrice)
				_stopPrice = candidate;
		}
		else
		{
			var candidate = entry - (entry - take) * 0.5m;
			var limit = currentPrice + priceStep;
			if (limit >= entry)
				limit = entry;

			if (candidate < limit)
				candidate = limit;

			if (candidate < currentStop && candidate > currentPrice)
				_stopPrice = candidate;
		}
	}

	private void ResetRiskLevels()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}
}