Ver en GitHub

Commission Calculator Strategy

Overview

The Commission Calculator Strategy is a utility strategy that mirrors the original MetaTrader script. It sends a single discretionary order using the selected execution mode (market, limit, or stop) and measures the broker commission applied to every resulting fill. The strategy stores the cumulative commission and prints a final report with the starting balance, total fees, and fee-adjusted balance when it finishes.

Unlike conventional signal-driven strategies, no market data or indicators are required. The strategy focuses on automated fee accounting for manual or semi-manual executions.

Trading Logic

  1. When the strategy starts, it captures the initial portfolio balance and configures the default trade volume.
  2. Optional protective stop-loss and take-profit levels are activated through StartProtection when both the entry price and target prices are valid. The distances are calculated in absolute price units, mimicking the MQL implementation.
  3. The configured order mode is executed exactly once. If parameters are inconsistent (for example, missing entry price for limit orders), the strategy logs the issue and skips sending the order.
  4. Every own trade received through OnNewMyTrade is processed to calculate the commission fee using the configured percentage rate.
  5. The strategy aggregates all commissions, remembers the latest fee, and logs a detailed summary on stop.

The implementation assumes that the broker fee is proportional to price × volume × commissionRate / 100. Adjust the rate to match the venue being modeled.

Parameters

Name Default Description
Quantity 0.001 Trade volume sent by helper methods (BuyMarket, SellLimit, etc.).
EntryPrice 31365 Price used for limit or stop orders and for calculating protective distances.
StopLossPrice 31200 Price that defines the stop-loss distance. A non-positive distance disables the stop-loss protection.
TakeProfitPrice 32100 Price that defines the take-profit distance. A non-positive distance disables the take-profit protection.
CommissionRate 0.04 Commission rate expressed as a percentage of traded notional.
Mode None Order type to execute when the strategy starts. Options: None, MarketBuy, MarketSell, BuyLimit, SellLimit, BuyStop, SellStop.

Notes and Best Practices

  • Start the strategy on a portfolio that supports manual order placement; no data subscriptions are required.
  • Ensure that the broker commission model matches the CommissionRate parameter to avoid underestimating or overestimating fees.
  • For pending orders, set EntryPrice to a valid level before launching the strategy; otherwise the order is not submitted.
  • When protective levels are enabled, the strategy instructs the connector to use market exits upon trigger to closely mimic the original MQL behavior.

Result Reporting

When OnStopped is invoked, the strategy logs:

  • Initial balance snapshot (taken when the strategy started).
  • Aggregated brokerage fees for all processed trades.
  • Final balance adjusted by subtracting the accumulated fees.

This makes the strategy well suited for fast what-if analyses and for validating broker commission schedules during backtests.

namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Commission Calculator strategy: CCI level crossover.
/// Buys when CCI crosses above -100 (oversold exit), sells when CCI crosses below 100 (overbought exit).
/// </summary>
public class CommissionCalculatorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _period;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private decimal _prevCci;
	private int _candlesSinceTrade;
	private bool _hasPrev;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int Period { get => _period.Value; set => _period.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public CommissionCalculatorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_period = Param(nameof(Period), 30)
			.SetGreaterThanZero()
			.SetDisplay("Period", "CCI period", "Indicators");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevCci = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevCci = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
		var cci = new CommodityChannelIndex { Length = Period };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(cci, ProcessCandle).Start();
	}

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

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		if (_hasPrev)
		{
			if (_prevCci < -100 && cciValue >= -100 && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
			else if (_prevCci > 100 && cciValue <= 100 && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
		}

		_prevCci = cciValue;
		_hasPrev = true;
	}
}