GitHub で見る

PresentTrend

終値RSIまたは典型価格と出来高のMFIで制御する因果ATRラインです。2本前のライン値との確認済みクロスで、エントリー、完全反転、決済を行います。

ラインとシグナル

確定足のみで、既定は5分足です。標準Wilder ATRと選択したオシレーターは正の共通期間Length=14を使い、未選択のオシレーターは登録しません。両方が形成されてから計算します。

  • オシレーター ≥50:候補low - Multiplier * ATR。ラインは候補と前回ラインの最大値。
  • オシレーター <50:候補high + Multiplier * ATR。ラインは候補と前回ラインの最小値。
  • 最初のラインは選択した候補そのものとし、以前の値を仮定しません。Multiplier=1で正が必要です。

ATRは前回終値からのギャップを含み、拡大する平均から始まり、その後Wilder平滑化を使います。RSIは終値の上昇・下落をWilder平滑化し、終値不変なら前回結果を保持します。MFIはtypical=(high+low+close)/3と出来高の積を使い、typical上昇なら正流、下落なら負流、不変ならどちらにも計上しません。直近Length足で100 * positiveSum / (positiveSum + negativeSum)、負流ゼロなら100です。

tでの生ロングクロスはline[t-1] <= line[t-3]かつline[t] > line[t-2]、ショートはそれぞれ≥と<です。形成済みライン4本が必要です。

ロングの確認には、以前の生ショートが存在し、以前の生ロングより新しいこと(または生ロングがまだないこと)が必要です。ショートは対称です。比較後に各生シグナルを記録し、取引を拒否されたものも含めます。観測済みの反対シグナルがない最初の生シグナルは取引しません。同方向の繰り返しには間に反対シグナルが必要です。

執行と既定値

両方向有効なら、ポジションなしでVolume、反対ポジションならVolume + abs(Position)で完全反転します。同方向は保持します。ロング専用なら確認済みショートで全ロングを閉じ、ショートは開きません。ショート専用は対称です。無効方向でポジションなしなら何もしません。未完了注文は重複を防ぎますが、ラインと生シグナル履歴は進みます。拒否したシグナルを持ち越さず、6時間待機もありません。

独立した価格ストップロスや利確はありません。単方向モードの決済は確認済みの反対シグナルによるもので、ATR価格ストップ注文ではありません。

パラメーター 既定値
Length 14、正の共通期間
Multiplier 1、正のATR距離
IsUseMfi falseでRSI、trueでMFI
IsLongEnabled / IsShortEnabled true / true、少なくとも片側が有効
CandleType 5分

C#とPythonは既存BTC/TON履歴からATR、RSI/MFI、ライン、生・確認済みクロスを独立計算し、各注文と実際のポジションの時刻、方向、完全反転、単方向決済を検証します。両オシレーターと6時間未満のシグナル間隔を試します。収益性を証明しません。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

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

/// <summary>
/// Uses a causal ATR rail selected by RSI or MFI and confirmed crosses of its two-bar lag.
/// Opposite signals fully reverse or close a position when that entry direction is disabled.
/// </summary>
public class PresentTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<bool> _isUseMfi;
	private readonly StrategyParam<bool> _isLongEnabled;
	private readonly StrategyParam<bool> _isShortEnabled;
	private readonly StrategyParam<DataType> _candleType;
	private readonly Queue<decimal> _previousLines = new();
	private decimal? _line;
	private int _barIndex;
	private int? _lastLongSignal;
	private int? _lastShortSignal;
	private Order _pendingOrder;

	/// <summary>Initializes period14, ATR multiplier1, RSI and both entry directions on five-minute candles.</summary>
	public PresentTrendStrategy()
	{
		_length = Param(nameof(Length), 14).SetGreaterThanZero()
			.SetDisplay("Length", "Shared Wilder ATR and selected RSI/MFI period", "Indicators");
		_multiplier = Param(nameof(Multiplier), 1m).SetGreaterThanZero()
			.SetDisplay("Multiplier", "ATR distance of the causal low/high rail", "Indicators");
		_isUseMfi = Param(nameof(IsUseMfi), false)
			.SetDisplay("Use MFI", "Use volume-weighted typical-price MFI instead of close RSI", "Indicators");
		_isLongEnabled = Param(nameof(IsLongEnabled), true)
			.SetDisplay("Enable Long", "Allow long entries; short positions still close on an opposite signal", "Trading");
		_isShortEnabled = Param(nameof(IsShortEnabled), true)
			.SetDisplay("Enable Short", "Allow short entries; long positions still close on an opposite signal", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Finished candles supplying the rail and oscillator", "General");
		OrderRegistering += order => _pendingOrder = order;
	}

	/// <summary>Positive shared ATR and RSI/MFI period.</summary>
	public int Length { get => _length.Value; set => _length.Value = value; }

	/// <summary>Positive ATR multiplier for the rail.</summary>
	public decimal Multiplier { get => _multiplier.Value; set => _multiplier.Value = value; }

	/// <summary>Whether to use native typical-price/volume MFI; otherwise native close RSI.</summary>
	public bool IsUseMfi { get => _isUseMfi.Value; set => _isUseMfi.Value = value; }

	/// <summary>Whether long entries are allowed; opposite signals can still close shorts.</summary>
	public bool IsLongEnabled { get => _isLongEnabled.Value; set => _isLongEnabled.Value = value; }

	/// <summary>Whether short entries are allowed; opposite signals can still close longs.</summary>
	public bool IsShortEnabled { get => _isShortEnabled.Value; set => _isShortEnabled.Value = value; }

	/// <summary>Finished candle type; five minutes by default.</summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

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

	private void ResetState()
	{
		_previousLines.Clear();
		_line = null;
		_barIndex = 0;
		_lastLongSignal = _lastShortSignal = null;
		_pendingOrder = null;
	}

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

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		if (Length <= 0 || Multiplier <= 0m || !IsLongEnabled && !IsShortEnabled)
			throw new ArgumentException("Period and multiplier must be positive and at least one entry direction enabled.");
		ResetState();
		var atr = new AverageTrueRange { Length = Length };
		IIndicator oscillator = IsUseMfi ? new MoneyFlowIndex { Length = Length } : new RelativeStrengthIndex { Length = Length };
		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx([atr, oscillator], ProcessCandle, allowEmpty: true).Start();
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
	{
		if (candle.State != CandleStates.Finished || values.Any(value => value.IsEmpty || !value.IsFormed))
			return;
		var bullish = values[1].ToDecimal() >= 50m;
		var candidate = bullish ? candle.LowPrice - Multiplier * values[0].ToDecimal() : candle.HighPrice + Multiplier * values[0].ToDecimal();
		var line = _line is decimal previous ? bullish ? Math.Max(previous, candidate) : Math.Min(previous, candidate) : candidate;
		var history = _previousLines.ToArray();
		Sides? signal = null;
		if (history.Length == 3)
		{
			if (history[2] <= history[0] && line > history[1])
			{
				if (_lastShortSignal is int lastShort && (_lastLongSignal is null || lastShort > _lastLongSignal))
					signal = Sides.Buy;
				_lastLongSignal = _barIndex;
			}
			else if (history[2] >= history[0] && line < history[1])
			{
				if (_lastLongSignal is int lastLong && (_lastShortSignal is null || lastLong > _lastShortSignal))
					signal = Sides.Sell;
				_lastShortSignal = _barIndex;
			}
		}
		_line = line;
		_previousLines.Enqueue(line);
		if (_previousLines.Count > 3)
			_previousLines.Dequeue();
		_barIndex++;
		if (signal is not Sides side || !IsFormedAndOnlineAndAllowTrading()
			|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed)
			|| side == Sides.Buy && Position > 0m || side == Sides.Sell && Position < 0m)
			return;
		var enabled = side == Sides.Buy ? IsLongEnabled : IsShortEnabled;
		if (!enabled && Position == 0m)
			return;
		RegisterOrder(new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = Math.Abs(Position) + (enabled ? Volume : 0m),
			Comment = enabled ? "PresentTrend confirmed cross" : "PresentTrend opposite close",
		});
	}
}