Auf GitHub ansehen

Money Fixed Margin Strategy

This strategy replicates the MetaTrader "Money Fixed Margin" example using StockSharp's high-level API. It showcases how to size positions by risking a fixed percentage of the portfolio while converting stop-loss distance expressed in pips to an absolute price offset. The strategy only trades long positions and focuses on demonstrating the money management logic rather than a predictive entry signal.

Details

  • Entry Criteria:
    • Long: executes a market buy after every completed candle count specified by Check Interval (defaults to every 980th bar). The order uses the closing price of the triggering candle as the reference for risk calculations.
  • Long/Short: Long only.
  • Exit Criteria:
    • Protective stop-loss is automatically attached via StartProtection at a distance derived from the Stop Loss (pips) parameter.
    • No profit target is used; positions close only by the stop-loss or manual intervention.
  • Stops: Stop Loss only.
  • Default Values:
    • Stop Loss (pips) = 25
    • Risk Percent = 10
    • Check Interval = 980
    • Candle Type = 1-minute time frame
  • Filters:
    • Category: Risk Management
    • Direction: Long
    • Indicators: None
    • Stops: Yes (stop-loss)
    • Complexity: Basic
    • Timeframe: Intraday (configurable through Candle Type)
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium (scales with Risk Percent)

Position Sizing Logic

  1. The strategy reads Security.PriceStep and Security.Decimals to infer the pip size. Pairs with 3 or 5 decimal places use a tenfold multiplier to match MetaTrader's definition of a pip.
  2. Stop Loss (pips) is multiplied by the pip size to obtain an absolute price distance (ExtStopLoss) identical to the MQL5 code.
  3. The current portfolio value (preferring Portfolio.CurrentValue then Portfolio.BeginValue) is multiplied by Risk Percent / 100 to determine the capital exposed per trade.
  4. Risk per single lot is computed through the product of the stop-loss distance, the number of price steps within that distance, and Security.StepPrice when available. If StepPrice is unknown, the price distance itself is used as a fallback.
  5. Dividing the risk amount by the risk per lot yields the desired volume. The result is normalized to the security's VolumeStep, clamped to minimum and maximum volume limits, and logged for transparency. A comparison value with zero stop-loss distance is also logged to illustrate why the money manager refuses trades without a protective stop.

Workflow

  1. On start the strategy subscribes to the configured candle series, calculates the pip size, and enables StartProtection with the computed absolute stop-loss distance.
  2. Each finished candle increments an internal counter. When the counter reaches the chosen Check Interval, the strategy evaluates position size, prints diagnostic information, and resets the counter.
  3. If the computed volume is positive, a market buy order is placed. The built-in protection attaches the stop-loss at Close - ExtStopLoss. Any errors (e.g., due to insufficient data or zero-priced instruments) prevent order submission.
  4. No further trades are taken until the counter completes another interval, keeping the focus on money management rather than signal frequency.

Usage Notes

  • Set Risk Percent to a conservative value when connecting to a live account; the default 10% risk mirrors the MQL example but is aggressive for real trading.
  • Ensure that the security provides meaningful PriceStep and StepPrice metadata. When unavailable, the strategy still operates but interprets risk in raw price units.
  • The strategy intentionally avoids short trades to stay faithful to the original demonstration. Adapt BuyMarket/SellMarket calls if two-sided trading is desired.
  • Combine this money management module with other signal generators by reusing the CalculateFixedMarginVolume helper from the strategy code.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Recreates the MetaTrader Money Fixed Margin sample using StockSharp.
/// It demonstrates fixed percentage risk sizing for long trades.
/// </summary>
public class MoneyFixedMarginStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _checkInterval;
	private readonly StrategyParam<DataType> _candleType;

	private int _barCount;
	private decimal _pipSize;

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

	/// <summary>
	/// Portfolio percentage risked on each trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Number of finished candles between trade attempts.
	/// </summary>
	public int CheckInterval
	{
		get => _checkInterval.Value;
		set => _checkInterval.Value = value;
	}

	/// <summary>
	/// Candle series used to time entries.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MoneyFixedMarginStrategy"/>.
	/// </summary>
	public MoneyFixedMarginStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 25m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Percent", "Percent of equity risked per trade", "Risk");

		_checkInterval = Param(nameof(CheckInterval), 150)
			.SetGreaterThanZero()
			.SetDisplay("Check Interval", "Completed candles between trades", "Execution");

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_barCount = 0;
		_pipSize = 0m;
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		_pipSize = priceStep * adjust;
		if (_pipSize <= 0m)
			_pipSize = priceStep > 0m ? priceStep : 1m;

		// Attach a protective stop using the pip-based distance converted to price units.
		StartProtection(
			new Unit(StopLossPips * _pipSize, UnitTypes.Absolute),
			new Unit(StopLossPips * _pipSize * 2, UnitTypes.Absolute));

		// Subscribe to the candle stream that emulates the tick counter from the MQL example.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

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

		// Count finished candles to mirror the tick counter from the original script.
		_barCount++;

		if (_barCount < CheckInterval)
			return;

		var entryPrice = candle.ClosePrice;

		if (entryPrice <= 0m)
		{
			LogWarning("Skip trade because entry price is not positive. Close={0}", entryPrice);
			return;
		}

		var riskAmount = CalculateRiskAmount();
		if (riskAmount <= 0m)
		{
			LogWarning("Skip trade because risk amount is not positive. Portfolio value={0}", riskAmount);
			return;
		}

		var stopDistance = StopLossPips * _pipSize;
		var stopPrice = entryPrice - stopDistance;

		var volumeWithoutStop = CalculateFixedMarginVolume(entryPrice, 0m, riskAmount);
		var volumeWithStop = CalculateFixedMarginVolume(entryPrice, stopPrice, riskAmount);

		this.LogInfo(
			"StopLoss=0 -> volume {0:0.####}; StopLoss={1:0.#####} -> volume {2:0.####}; Portfolio={3:0.##}",
			volumeWithoutStop,
			stopPrice,
			volumeWithStop,
			GetPortfolioValue());

		BuyMarket();

		// Reset the counter only after successfully sending an order.
		_barCount = 0;
	}

	private decimal CalculateRiskAmount()
	{
		var portfolioValue = GetPortfolioValue();
		return portfolioValue > 0m ? portfolioValue * RiskPercent / 100m : 0m;
	}

	private decimal GetPortfolioValue()
	{
		var current = Portfolio?.CurrentValue ?? 0m;
		if (current > 0m)
			return current;

		var begin = Portfolio?.BeginValue ?? 0m;
		return begin > 0m ? begin : current;
	}

	private decimal CalculateFixedMarginVolume(decimal entryPrice, decimal stopPrice, decimal riskAmount)
	{
		if (riskAmount <= 0m || entryPrice <= 0m || stopPrice <= 0m)
			return 0m;

		var stopDistance = entryPrice - stopPrice;
		if (stopDistance <= 0m)
			return 0m;

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

		var stepPrice = 0m;
		if (stepPrice <= 0m)
			stepPrice = priceStep;

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

		var riskPerVolume = stepsCount * stepPrice;
		if (riskPerVolume <= 0m)
			return 0m;

		var rawVolume = riskAmount / riskPerVolume;
		return NormalizeVolume(rawVolume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		if (Security?.VolumeStep is decimal step && step > 0m)
		{
			volume = Math.Ceiling(volume / step) * step;
		}

		return volume;
	}
}