GitHub で見る

スターター 2005 戦略

概要

Starter 2005 Strategy は、2005 年にリリースされた古典的な MetaTrader 4 エキスパート アドバイザ Starter.mq4 の StockSharp の高レベル API 変換です。元のシステムには、ラゲール オシレーター、指数移動平均傾斜フィルター、および商品チャネル指数確認が混合されていました。このポートは、資金管理と実行を StockSharp の規則に適応させながら、デシジョン ツリーをそのまま維持します。

  • ラゲール RSI プロキシは、0 と 1 の間で振動する iCustom("Laguerre") バッファを複製します。
  • 中央価格に基づいて計算された 5 期間の EMA は、MT4 エキスパートが使用するのと同じ上昇/下降勾配の確認を提供します。
  • 終値で測定される 14 期間の CCI は、元の Alpha 変数と同様に、弱い設定を除外します。
  • 適応型ロットサイジングルーチンは、連続損失後の連続ベースの削減など、過去の LotsOptimized() 関数を反映しています。
  • ポジションの決済は、ラゲールがエクストリームゾーンから反転するか、トレードが Point * Stop に相当する設定可能な利益距離に達することによってトリガーされます。

取引ロジック

  1. インジケーターの準備
    • ラゲール RSI 値は、構成可能な Gamma を使用した 4 段階のラゲール フィルターを通じて再構築されます。
    • EMA の長さのデフォルトは 5 つのローソク足で、(High + Low) / 2 を演算して MQL4 の PRICE_MEDIAN と一致します。
    • CCI 期間のデフォルトは終値で 14 で、従来のコードを忠実に保つために非常に小さなしきい値 (±5) が維持されます。
  2. 長いセットアップ
    • ラゲールはゼロに近い値でなければなりません (LaguerreEntryTolerance は厳密な == 0 比較をエミュレートします)。
    • EMA は、以前に完成したローソク足と比較して上昇しているはずです。
    • CCI は -CciThreshold を下回る必要があります。
  3. 簡単なセットアップ
    • ラゲールは 1 の近くに座らなければなりません (1 - LaguerreEntryTolerance== 1 に近似します)。
    • EMAは落ちているに違いありません。
    • CCI は +CciThreshold を超える必要があります。
  4. 出口
    • ラゲールが LaguerreExitHigh (デフォルトは 0.9) を超えて上昇したとき、または価格がエントリーから TakeProfitPoints * PriceStep 進んだときにロングは閉じます。
    • ラゲール株価が LaguerreExitLow (デフォルトは 0.1) を下回るか、価格が同じ距離だけ下落すると、ショートは終了します。
    • その他の手動のフラット位置では、内部状態が自動的にリセットされ、入力データが古くならないようにします。

お金の管理

CalculateOrderVolume ヘルパーは、MT4 LotsOptimized() の動作を再現します。

  1. リスクベースのサイジング – 資本に MaximumRisk を掛けた値を RiskDivider で割ります (元の /500 ルールと同様、デフォルトは 500)。現在の価格で割ると、リスク調整後のロットサイズが求められます。
  2. フォールバック ロット – リスクサイジングによって生成される数値が BaseVolume より小さい場合、アルゴリズムは基本ロットを維持します。
  3. 連敗削減 – 2 回以上連続して負けた取引の後、取引履歴を検査した MQL ループと正確に一致して、取引量が volume * losses / DecreaseFactor だけ減少します。
  4. 正規化 – ボリュームは商品の VolumeStep に正規化され、注文の拒否を避けるために MinVolumeMaxVolume の間にクランプされます。

連続損失追跡は利益が出た後にリセットされ、取引に負けた後に増加します。損益分岐点の結果はカウンターをそのままにし、利益ゼロのチケットを無視した元の動作を反映しています。

パラメーター

名前 種類 デフォルト 説明
BaseVolume decimal 1.2 リスクサイジングでより少ない金額が推奨される場合に使用される最小ロットサイズ。
MaximumRisk decimal 0.036 ディバイダーを適用する前に新しいポジションで公開された株式の割合。
RiskDivider decimal 500 除数はリスク資本に適用され、元の AccountFreeMargin() * MaximumRisk / 500 ルールが再現されます。
DecreaseFactor decimal 2 連続損失の後にボリュームを縮小するために使用されるストリーク除数。
MaPeriod int 5 ローソク足中央価格の EMA の長さ。
CciPeriod int 14 コモディティチャネル指数のルックバック。
CciThreshold decimal 5 信号をトリガーするには絶対 CCI レベルが必要です。
LaguerreGamma decimal 0.66 ラゲール フィルターの平滑化係数。
LaguerreEntryTolerance decimal 0.02 0/1 付近の許容値は、元の等価性チェックを模倣するために使用されていました。
LaguerreExitHigh decimal 0.9 ロングポジションの上部出口レベル。
LaguerreExitLow decimal 0.1 ショートポジションの出口レベルを下げる。
TakeProfitPoints decimal 10 価格ポイントで表される利益目標 (MQL の Point * Stop)。
CandleType DataType TimeFrame(5m) キャンドルのサブスクリプションは戦略によって処理されます。

実装メモ

  • Laguerre RSI は、元のインジケーターからの 4 レベルの再帰を使用してインラインで実装されます。 GetValue() を呼び出す必要はありません。
  • EMA および CCI インジケーターは、中央値フィードが MetaTrader の PRICE_MEDIAN オプションと一致することを保証するために、ローソク足コールバック内で手動で更新されます。
  • マーケットエントリーは、AllowLong() / AllowShort() フラグを尊重し、保留中のアクティブな注文がないことを保証し、ソース EA の単一ポジション設計を維持します。
  • 取引結果の追跡では、ローソク足の決定価格 (最終価格、終値、始値) を使用して損益の方向を推定し、連敗カウンターを維持します。
  • 英語のインライン コメントは、将来のメンテナンスに役立つすべての主要な決定ブロックを説明します。

使い方のヒント

  • 元の EA は日中の外国為替チャートを対象としていました。 10 ポイントの利益目標が 1 ピップに一致するように、小さな価格ステップを提供する流動的な商品から始めます。
  • MT4 スクリプトは常に 1 つのポジションのみを保持するため、部分約定や同時注文が起こりにくい環境 (過去のテストまたは流動的な市場) でストラテジーを実行してください。
  • オシレーターがデータセット上で正確に 0 または 1 にほとんど触れない場合は、LaguerreEntryTolerance を調整します。
  • RiskDividerDecreaseFactor を一緒に調整して、リスクの増大と損失の軽減のバランスをとります。
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>
/// Conversion of the MetaTrader 4 expert advisor "Starter" (2005 release).
/// Combines a Laguerre RSI proxy, EMA slope confirmation and a CCI filter.
/// Implements adaptive lot sizing inspired by the original LotsOptimized routine.
/// </summary>
public class Starter2005Strategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _riskDivider;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciThreshold;
	private readonly StrategyParam<decimal> _laguerreGamma;
	private readonly StrategyParam<decimal> _laguerreEntryTolerance;
	private readonly StrategyParam<decimal> _laguerreExitHigh;
	private readonly StrategyParam<decimal> _laguerreExitLow;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema = null!;
	private CommodityChannelIndex _cci = null!;

	private decimal? _previousMa;
	private decimal _lagL0;
	private decimal _lagL1;
	private decimal _lagL2;
	private decimal _lagL3;
	private bool _laguerreFormed;

	private decimal? _entryPrice;
	private decimal _entryVolume;
	private Sides? _entrySide;
	private int _consecutiveLosses;

	/// <summary>
	/// Initializes a new instance of the <see cref="Starter2005Strategy"/> class.
	/// </summary>
	public Starter2005Strategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1.2m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Initial lot size used when risk-based sizing is unavailable", "Risk Management")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_maximumRisk = Param(nameof(MaximumRisk), 0.036m)
			.SetNotNegative()
			.SetDisplay("Maximum Risk", "Fraction of account equity considered for sizing", "Risk Management")
			
			.SetOptimize(0m, 0.1m, 0.005m);

		_riskDivider = Param(nameof(RiskDivider), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Divider", "Divisor applied to risk capital (mimics the original /500 rule)", "Risk Management")
			
			.SetOptimize(100m, 1000m, 50m);

		_decreaseFactor = Param(nameof(DecreaseFactor), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management")
			
			.SetOptimize(1m, 5m, 0.5m);

		_maPeriod = Param(nameof(MaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Length of the exponential moving average applied to median price", "Indicators")
			
			.SetOptimize(3, 30, 1);

		_cciPeriod = Param(nameof(CciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Commodity Channel Index lookback length", "Indicators")
			
			.SetOptimize(5, 40, 1);

		_cciThreshold = Param(nameof(CciThreshold), 5m)
			.SetNotNegative()
			.SetDisplay("CCI Threshold", "Absolute CCI level required for signals", "Indicators")
			
			.SetOptimize(1m, 50m, 1m);

		_laguerreGamma = Param(nameof(LaguerreGamma), 0.66m)
			.SetRange(0.1m, 0.9m)
			.SetDisplay("Laguerre Gamma", "Smoothing factor of the Laguerre RSI filter", "Indicators")
			
			.SetOptimize(0.3m, 0.9m, 0.05m);

		_laguerreEntryTolerance = Param(nameof(LaguerreEntryTolerance), 0.02m)
			.SetRange(0m, 0.3m)
			.SetDisplay("Laguerre Entry Tolerance", "Closeness to 0/1 required to mimic the original equality checks", "Signals")
			
			.SetOptimize(0.005m, 0.1m, 0.005m);

		_laguerreExitHigh = Param(nameof(LaguerreExitHigh), 0.9m)
			.SetRange(0.5m, 1m)
			.SetDisplay("Laguerre Exit High", "Upper exit level for long positions", "Signals")
			
			.SetOptimize(0.6m, 1m, 0.05m);

		_laguerreExitLow = Param(nameof(LaguerreExitLow), 0.1m)
			.SetRange(0m, 0.5m)
			.SetDisplay("Laguerre Exit Low", "Lower exit level for short positions", "Signals")
			
			.SetOptimize(0m, 0.4m, 0.05m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance in price points before profit is locked", "Risk Management")
			
			.SetOptimize(0m, 50m, 5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe processed by the strategy", "General");
	}

	/// <summary>
	/// Base lot size used when the risk model produces a smaller value.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of the portfolio considered for risk-based sizing.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Divider applied to the risk capital (mirrors the /500 rule).
	/// </summary>
	public decimal RiskDivider
	{
		get => _riskDivider.Value;
		set => _riskDivider.Value = value;
	}

	/// <summary>
	/// Lot reduction factor after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// EMA length applied to median price.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// CCI lookback period.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Absolute CCI level required for entry.
	/// </summary>
	public decimal CciThreshold
	{
		get => _cciThreshold.Value;
		set => _cciThreshold.Value = value;
	}

	/// <summary>
	/// Laguerre smoothing factor (gamma).
	/// </summary>
	public decimal LaguerreGamma
	{
		get => _laguerreGamma.Value;
		set => _laguerreGamma.Value = value;
	}

	/// <summary>
	/// Tolerance applied when checking Laguerre against 0 or 1.
	/// </summary>
	public decimal LaguerreEntryTolerance
	{
		get => _laguerreEntryTolerance.Value;
		set => _laguerreEntryTolerance.Value = value;
	}

	/// <summary>
	/// Laguerre exit threshold for long positions.
	/// </summary>
	public decimal LaguerreExitHigh
	{
		get => _laguerreExitHigh.Value;
		set => _laguerreExitHigh.Value = value;
	}

	/// <summary>
	/// Laguerre exit threshold for short positions.
	/// </summary>
	public decimal LaguerreExitLow
	{
		get => _laguerreExitLow.Value;
		set => _laguerreExitLow.Value = value;
	}

	/// <summary>
	/// Profit distance expressed in price points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_ema = null!;
		_cci = null!;
		_previousMa = null;
		_lagL0 = _lagL1 = _lagL2 = _lagL3 = 0m;
		_laguerreFormed = false;
		_entryPrice = null;
		_entryVolume = 0m;
		_entrySide = null;
		_consecutiveLosses = 0;
	}

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

		_ema = new EMA { Length = MaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };

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

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

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

		// Process CCI manually
		_cci.Process(candle);

		if (!_ema.IsFormed || !_cci.IsFormed)
		{
			_previousMa = ma;
			return;
		}

		var cci = _cci.GetCurrentValue<decimal>();

		var laguerre = CalculateLaguerre(candle.ClosePrice);
		if (!_laguerreFormed)
		{
			_previousMa = ma;
			return;
		}

		var previousMa = _previousMa;
		_previousMa = ma;
		if (!previousMa.HasValue)
			return;

		var maRising = ma > previousMa.Value;
		var maFalling = ma < previousMa.Value;
		var entryTolerance = LaguerreEntryTolerance;
		var takeProfitDistance = GetTakeProfitDistance();
		var price = GetDecisionPrice(candle);

		if (Position == 0m && !HasActiveOrders())
		{
			if (maRising && laguerre <= entryTolerance && cci < -CciThreshold)
			{
				var volume = CalculateOrderVolume(price);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entrySide = Sides.Buy;
					_entryPrice = price;
					_entryVolume = volume;
					LogInfo($"Opening long. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA rising.");
				}
			}
			else if (maFalling && laguerre >= 1m - entryTolerance && cci > CciThreshold)
			{
				var volume = CalculateOrderVolume(price);
				if (volume > 0m)
				{
					SellMarket(volume);
					_entrySide = Sides.Sell;
					_entryPrice = price;
					_entryVolume = volume;
					LogInfo($"Opening short. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA falling.");
				}
			}
		}

		if (_entrySide == Sides.Buy && Position > 0m && _entryPrice.HasValue)
		{
			var gain = price - _entryPrice.Value;
			if ((LaguerreExitHigh > 0m && laguerre >= LaguerreExitHigh) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
			{
				var volume = Math.Abs(Position);
				if (volume <= 0m)
					volume = _entryVolume;

				if (volume > 0m && !HasActiveOrders())
				{
					SellMarket(volume);
					RegisterTradeResult(gain);
					ResetPositionState();
					LogInfo($"Closing long. Laguerre={laguerre:F4}, gain={gain:F5}.");
				}
			}
		}
		else if (_entrySide == Sides.Sell && Position < 0m && _entryPrice.HasValue)
		{
			var gain = _entryPrice.Value - price;
			if ((LaguerreExitLow > 0m && laguerre <= LaguerreExitLow) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
			{
				var volume = Math.Abs(Position);
				if (volume <= 0m)
					volume = _entryVolume;

				if (volume > 0m && !HasActiveOrders())
				{
					BuyMarket(volume);
					RegisterTradeResult(gain);
					ResetPositionState();
					LogInfo($"Closing short. Laguerre={laguerre:F4}, gain={gain:F5}.");
				}
			}
		}
		else if (Position == 0m && !HasActiveOrders())
		{
			ResetPositionState();
		}
	}

	private decimal CalculateLaguerre(decimal price)
	{
		var gamma = LaguerreGamma;
		var l0Prev = _lagL0;
		var l1Prev = _lagL1;
		var l2Prev = _lagL2;
		var l3Prev = _lagL3;

		_lagL0 = (1m - gamma) * price + gamma * l0Prev;
		_lagL1 = -gamma * _lagL0 + l0Prev + gamma * l1Prev;
		_lagL2 = -gamma * _lagL1 + l1Prev + gamma * l2Prev;
		_lagL3 = -gamma * _lagL2 + l2Prev + gamma * l3Prev;

		decimal cu = 0m;
		decimal cd = 0m;

		if (_lagL0 >= _lagL1)
			cu = _lagL0 - _lagL1;
		else
			cd = _lagL1 - _lagL0;

		if (_lagL1 >= _lagL2)
			cu += _lagL1 - _lagL2;
		else
			cd += _lagL2 - _lagL1;

		if (_lagL2 >= _lagL3)
			cu += _lagL2 - _lagL3;
		else
			cd += _lagL3 - _lagL2;

		var denominator = cu + cd;
		var result = denominator == 0m ? 0m : cu / denominator;

		_laguerreFormed = true;
		return result;
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		var volume = BaseVolume;

		if (MaximumRisk > 0m && RiskDivider > 0m)
		{
			var portfolio = Portfolio;
			var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
			if (equity > 0m && price > 0m)
			{
				var riskVolume = equity * MaximumRisk / RiskDivider;
				riskVolume /= price;
				if (riskVolume > volume)
					volume = riskVolume;
			}
		}

		if (DecreaseFactor > 0m && _consecutiveLosses > 1)
		{
			var reduction = volume * _consecutiveLosses / DecreaseFactor;
			volume -= reduction;
		}

		return NormalizeVolume(volume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep ?? 0m;
			if (step <= 0m)
				step = 1m;

			var minVolume = security.MinVolume ?? step;
			var maxVolume = security.MaxVolume;

			var steps = decimal.Floor(volume / step);
			if (steps < 1m)
				steps = 1m;

			volume = steps * step;

			if (volume < minVolume)
				volume = minVolume;

			if (maxVolume is decimal max && max > 0m && volume > max)
				volume = max;
		}

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

		return volume;
	}

	private decimal GetTakeProfitDistance()
	{
		if (TakeProfitPoints <= 0m)
			return 0m;

		var point = Security?.PriceStep ?? 0m;
		if (point <= 0m)
		{
			var decimals = Security?.Decimals ?? 4;
			point = 1m;
			for (var i = 0; i < decimals; i++)
				point /= 10m;
		}

		return TakeProfitPoints * point;
	}

	private decimal GetDecisionPrice(ICandleMessage candle)
	{
		if (candle.ClosePrice > 0m)
			return candle.ClosePrice;

		return candle.OpenPrice;
	}

	private bool HasActiveOrders()
	{
		foreach (var order in Orders)
		{
			if (order.State == OrderStates.Active)
				return true;
		}

		return false;
	}

	private void RegisterTradeResult(decimal gain)
	{
		if (gain > 0m)
		{
			if (_consecutiveLosses > 0)
				LogInfo($"Profit resets loss streak of {_consecutiveLosses} trades.");

			_consecutiveLosses = 0;
		}
		else if (gain < 0m)
		{
			_consecutiveLosses++;
			LogInfo($"Loss streak increased to {_consecutiveLosses}.");
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_entryVolume = 0m;
		_entrySide = null;
	}
}