在 GitHub 上查看

风险监控策略

概述

风险监控策略移植自 MetaTrader 4 专家顾问 risk.mq4。原始脚本并不会自动下单,而是根据账户余额和用户定义的风险百分比 计算可以安全使用的手数。本版本延续这一思路:持续分析账户状态、计算建议仓位规模、跟踪浮动及已实现收益,并将结果实时 写入策略注释,方便交易者快速查看。

与常规策略不同,风险监控策略不会自动发单。它承担的是监管职责:提供当前敞口、在选定风险预算下仍可使用的仓位额度,以及 已平仓交易的盈亏情况。每当持仓、收益或成交发生变化时,注释都会更新,确保信息始终反映最新的投资组合状态。

计算方法

策略在注释中展示的数值主要来自三类计算:

  1. 基础手数 – 按 账户余额 / 1000 计算,并根据标的的交易量步长进行对齐。该规则与 MT4 版本一致,即每 1000 账户货币 对应 1 标准手。
  2. 风险手数 – 将基础手数乘以 风险百分比 / 100,并对齐到交易量步长,得到在当前风险预算下可使用的仓位。
  3. 当前手数与差值 – 将净持仓绝对值与风险手数比较。如果尚未触及上限,差值表示在风险范围内仍可增加的手数。若差值为 很小的负值且小于交易量步长,则会被四舍五入为 0,避免显示噪声。

收益方面区分浮动收益和已实现收益:

  • 浮动收益 – 直接读取策略的 PnL 属性,同时给出货币数值和占当前资产的百分比。
  • 已实现收益 – 通过监听自身成交累加。组件会将每笔平仓成交拆分为盈利与亏损部分,扣除成交报告中的佣金,并持续累加。 最终结果也会折算成权益百分比,与 MT4 中的表现保持一致。

参数

  • 风险百分比 – 允许用于开新仓的账户余额比例,默认值 10。该参数支持优化,可快速回测不同风险预算。

注释格式

策略注释分三行显示:

  1. Base lotsRisk lotsOpen lotsLots to adjust – 手数和剩余容量概览。
  2. RiskFloating PnL – 风险设置、浮动盈亏(货币)以及相对于余额的百分比。
  3. Realized profit – 累计平仓盈亏及其百分比。

所有数值都遵循原脚本的舍入方式,会参考标的的交易量步长,并对货币数值保留两位小数。信息直接显示在注释中,无需额外面板 即可在图表或策略列表中查看。

使用提示

  • 将策略附加到需要监控的标的上;它与 StockSharp 一样使用净仓位模式(不支持 MT4 风格的对冲持仓)。
  • 策略支持人工交易:只要收到自己的成交回报,就会立即更新统计数据。
  • 当策略停止或重置时会自动清空注释,避免旧数据在会话之间残留。
  • 目前仅提供 C# 版本,API 包中没有 Python 实现。
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>
/// Risk-aware EMA crossover strategy inspired by the original MT4 Risk Monitor script.
/// Uses a risk percentage of account balance to size positions, combined with EMA crossover signals.
/// Tracks realized gains/losses and adjusts lot sizing accordingly.
/// </summary>
public class RiskMonitorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;

	private decimal? _prevFast;
	private decimal? _prevSlow;

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

	/// <summary>
	/// Percentage of the account balance allocated to new positions.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Take profit distance in absolute points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance in absolute points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="RiskMonitorStrategy"/>.
	/// </summary>
	public RiskMonitorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for calculations", "General");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portion of balance used to size positions", "Risk Management")
			.SetOptimize(5m, 30m, 5m);

		_fastPeriod = Param(nameof(FastPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
			.SetOptimize(5, 30, 5);

		_slowPeriod = Param(nameof(SlowPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
			.SetOptimize(20, 80, 10);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Absolute take profit distance", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Absolute stop loss distance", "Risk");

		Volume = 1;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_fastEma = null;
		_slowEma = null;
		_prevFast = null;
		_prevSlow = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_fastEma = new ExponentialMovingAverage { Length = FastPeriod };
		_slowEma = new ExponentialMovingAverage { Length = SlowPeriod };

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

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

		if (TakeProfitPoints > 0 || StopLossPoints > 0)
		{
			var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
			var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
			StartProtection(tp, sl);
		}

		base.OnStarted2(time);
	}

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

		var pf = _prevFast;
		var ps = _prevSlow;
		_prevFast = fastValue;
		_prevSlow = slowValue;

		if (pf == null || ps == null)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		// Detect crossover
		var prevDiff = pf.Value - ps.Value;
		var currDiff = fastValue - slowValue;

		if (prevDiff <= 0 && currDiff > 0)
		{
			// Bullish crossover
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			if (Position == 0)
				BuyMarket(Volume);
		}
		else if (prevDiff >= 0 && currDiff < 0)
		{
			// Bearish crossover
			if (Position > 0)
				SellMarket(Position);
			if (Position == 0)
				SellMarket(Volume);
		}
	}
}