GitHub で見る

Tengri Strategy (StockSharp Port)

This strategy is a high-level StockSharp recreation of the MetaTrader expert advisor Tengri. The original advisor trades EURUSD and USDCHF with a grid-and-scale approach driven by RSI, custom "Silence" volatility filters and an EMA trend gauge. The C# version keeps the behavioural core while adapting it to StockSharp conventions and net-position accounting.

Core Ideas

  • Directional bias – compare the current bid to the opening price of a higher timeframe candle (default 30 minutes). A positive difference biases the strategy long, a negative difference biases it short.
  • Momentum filter – a 14-period RSI calculated on hourly candles must stay below 70 for long entries and above 30 for short entries, matching the MetaTrader logic.
  • Quiet-market filters – the original custom "Silence" indicator is emulated with ATR values smoothed by EMAs on two different timeframes. Both filters must stay below configurable thresholds to permit entries or scale-ins.
  • Trend confirmation – an EMA on a medium timeframe ensures long additions only happen above the EMA and short additions only below it.
  • Grid and martingale sizing – the first trade uses either a fixed lot or an equity-proportional lot. Additional trades multiply the previous volume by configurable factors (1.70 before StepX, 2.08 afterwards by default).
  • Pip spacing – the distance between grid orders follows two base steps (10 pips and 20 pips by default) and can be grown exponentially through PipStepExponent.

Trading Workflow

  1. Entry evaluation (per EntryCandleType, default M1):
    • Determine direction from the DealCandleType candle.
    • Check RSI and the first silence filter.
    • Ensure no active trades in the same direction (opposite direction positions are flattened first because StockSharp portfolios are netted).
    • Submit a market order with the calculated lot size. The first position stores a pip-based take-profit target.
  2. Scale-in evaluation (per ScaleCandleType, default M1):
    • Confirm the EMA trend and the second silence filter.
    • Verify the last fill price is far enough from the current market using the pip-step rules.
    • Add another market order with martingale sizing while the direction stays valid and the trade count is below MaxTrades.
  3. Position management:
    • Optional global profit target closes the position when both long and short stacks exist and the combined unrealised PnL exceeds Equity / LimitDivisor.
    • The first trade's take-profit acts as a simple exit: when the bid/ask reaches the stored target the entire position is flattened.
    • No automatic stop-loss is used, mirroring the MetaTrader code.

Parameters

Parameter Description
DealCandleType Timeframe whose open price defines the directional bias.
EntryCandleType Timeframe for evaluating entry signals.
ScaleCandleType Timeframe for checking grid additions.
MaCandleType Timeframe for the EMA trend filter.
Silence1CandleType / Silence2CandleType Timeframes for ATR-based volatility filters.
RsiPeriod RSI length (default 14).
SilencePeriod1/2, SilenceInterpolation1/2, SilenceLevel1/2 ATR smoothing and thresholds controlling the two silence filters.
MaPeriod EMA period.
PipStep, PipStep2, PipStepExponent Distances between scale-in trades.
LotExponent1, LotExponent2, StepX Martingale factors for additional positions.
LotSize, FixLot, LotStep Money management settings for the first position.
SlTpPips Pip distance used to set a take-profit for the first trade (0 disables it).
MaxTrades Maximum number of entries per direction.
UseLimit, LimitDivisor Global profit lock configuration.
CloseFriday, CloseFridayHour Optional late-Friday entry lockout.

Differences from the MetaTrader Version

  • Silence indicator replacement – the proprietary "Silence" indicator is approximated with ATR values smoothed by EMAs. Thresholds retain the same numeric scale but can be tuned if the ATR proxy behaves differently.
  • Net position accounting – StockSharp portfolios are netted, so the strategy flattens the opposite direction before opening a new stack instead of hedging both sides simultaneously.
  • Take-profit handling – MetaTrader attaches TP only to the first order. The port closes the full net position when that target triggers. Additional orders intentionally have no TP, matching the original risk model.
  • Symbol choice – the strategy uses the Security assigned to the strategy instance. Configure separate instances for EURUSD, USDCHF or any other instrument.

Usage Notes

  • Configure the volume step and min/max volumes on the target security so that LotCheck style rounding aligns with broker requirements.
  • The strategy assumes the broker quotes provide best bid/ask updates. Without Level1 data the direction and TP checks cannot operate.
  • Because there is no stop-loss, consider running the strategy with external risk controls (equity stop, manual supervision, etc.).

Visualisation

To analyse the behaviour connect chart widgets to the subscribed candle series (direction, entry and scaling timeframes) plus overlay the EMA and ATR indicators. This mirrors the diagnostic tools used with the original advisor.

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Tengri strategy using WMA crossover with EMA trend filter.
/// Buys when fast WMA crosses above slow WMA and price above EMA.
/// Sells on reverse conditions.
/// </summary>
public class TengriStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private WeightedMovingAverage _fast;
	private WeightedMovingAverage _slow;
	private ExponentialMovingAverage _ema;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public TengriStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 15).SetGreaterThanZero().SetDisplay("Fast Period", "Fast WMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 60).SetGreaterThanZero().SetDisplay("Slow Period", "Slow WMA period", "Indicator");
		_emaPeriod = Param(nameof(EmaPeriod), 200).SetGreaterThanZero().SetDisplay("EMA Period", "Trend EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null; _ema = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new WeightedMovingAverage { Length = FastPeriod };
		_slow = new WeightedMovingAverage { Length = SlowPeriod };
		_ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, _ema, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal emaValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed || !_ema.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && close > emaValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 80; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && close < emaValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 80; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}