Самооптимизирующаяся стратегия RSI/MFI v3
Описание
Стратегия переносит советник MetaTrader «Self Optimizing RSI or MFI Trader» на высокоуровневый API StockSharp. После закрытия каждой свечи алгоритм перебирает OptimizingPeriods последних баров, тестирует все целые уровни между IndicatorBottomValue и IndicatorTopValue и определяет, на каких порогах индикатор приносил наибольшую прибыль для лонгов и шортов. В реальном времени сделки открываются только при пересечении текущим значением индикатора наиболее прибыльного уровня (или сразу в режиме UseAggressiveEntries). Выход осуществляется через стоп-лосс/тейк-профит по ATR или фиксированному расстоянию и необязательный перевод стопа в безубыток.
Данные
- Поддерживаются любые инструменты с OHLC-свечами; для MFI необходимы объёмы.
- Таймфрейм задаётся параметром
CandleType(по умолчанию 15 минут), можно выбрать любой доступный интервал.
Индикаторы
- RSI или MFI в зависимости от
IndicatorChoice, используют одинаковый периодIndicatorPeriod. - ATR нужен при включенном
UseDynamicTargetsдля расчёта стопов и целей.
Логика торговли
- Хранится окно из
OptimizingPeriods + 1завершённых свечей с индикаторными значениями и ценой закрытия. - Для каждого уровня внутри заданного диапазона проводится бэктест на окне: рассчитывается, какой исход (стоп или цель) наступил раньше при пробое уровня вниз (для шортов) и вверх (для лонгов).
- Выбираются уровни, показавшие максимальную историческую прибыль. Если включён
TradeReverse, прибыльность направлений меняется местами. - При пересечении индикатором лучшего уровня в прибыльном направлении (или немедленно в агрессивном режиме) открывается позиция, если позволяет
OneOrderAtATime. - Управление позицией:
- Стоп-лосс и тейк-профит вычисляются либо как ATR × множители
StopLossAtrMultiplier/TakeProfitAtrMultiplier, либо как фиксированное число пунктов (StaticStopLossPoints/StaticTakeProfitPoints). - Опция
UseBreakEvenпереносит стоп в точку входа плюсBreakEvenPaddingPoints, когда прибыль достигаетBreakEvenTriggerPoints. - Позиция закрывается при достижении стопа или цели.
- Стоп-лосс и тейк-профит вычисляются либо как ATR × множители
Риск-менеджмент
- Динамический размер: при включённом
UseDynamicVolumeобъём рассчитывается исходя из доли капиталаRiskPercent, используя параметры шага цены (PriceStep) и стоимости шага (StepPrice). - Фиксированный размер: при выключенной динамике каждая сделка открывается на
BaseVolumeконтрактов. - Безубыток: защищает прибыльные сделки после достижения заданного порога.
Параметры
| Параметр | Описание |
|---|---|
OptimizingPeriods |
Количество свечей в окне самооптимизации (по умолчанию 144). |
IndicatorChoice |
Выбор индикатора (RSI или MFI). |
IndicatorPeriod |
Период индикатора и ATR. |
IndicatorTopValue / IndicatorBottomValue |
Верхняя и нижняя границы перебираемых уровней. |
UseAggressiveEntries |
Вход без подтверждающего пересечения. |
TradeReverse |
Инвертирует предпочтение направлений. |
OneOrderAtATime |
Запрещает одновременные позиции при включении. |
UseDynamicTargets |
Переключатель между ATR- и пунктовыми стопами. |
StopLossAtrMultiplier, TakeProfitAtrMultiplier |
Множители ATR для динамических выходов. |
StaticStopLossPoints, StaticTakeProfitPoints |
Дистанции в пунктах при статических выходах. |
UseBreakEven, BreakEvenTriggerPoints, BreakEvenPaddingPoints |
Настройки перевода в безубыток. |
UseDynamicVolume, RiskPercent, BaseVolume |
Управление объёмом. |
CandleType |
Таймфрейм расчётов. |
Особенности реализации
- Используется подписка на свечи с
.Bind(...), поэтому расчёт выполняется только на завершённых барах. - В неттинговых счетах желательно удерживать
OneOrderAtATime = true, так как стратегия оперирует одной агрегированной позицией. - Торговля начинается после формирования всех необходимых значений ATR и выбранного индикатора.
- При выборе MFI требуется поток данных с объёмами, иначе индикатор не сформируется.
Рекомендации по настройке
- Подбирайте
OptimizingPeriods,IndicatorPeriodи ATR-множители вместе, чтобы адаптироваться к волатильности инструмента. - Сокращение диапазона уровней (например, 20–80) может снизить шум для боковых рынков.
- Рекомендуется проводить walk-forward тесты, так как стратегия постоянно адаптируется к свежим данным.
Порядок использования
- Подключите стратегию к нужному инструменту и портфелю в Designer или программно.
- Настройте параметры, уровни риска и размер позиции.
- Запустите стратегию — после накопления достаточного числа свечей начнут появляться сделки.
Ограничения
- Самооптимизация запускается на каждом баре, что может быть ресурсоёмко при больших окнах или широком диапазоне уровней.
- Перебор выполняется по целым уровням, дробные значения не тестируются.
- Предполагается устойчивость недавней динамики рынка; при смене режима стоит пересмотреть параметры или остановить стратегию.
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.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that dynamically optimizes RSI or MFI threshold levels over a rolling history window.
/// Chooses the most profitable overbought/oversold levels and executes trades with ATR or point based risk control.
/// </summary>
public class SelfOptimizingRsiOrMfiTraderV3Strategy : Strategy
{
private readonly StrategyParam<int> _optimizingPeriods;
private readonly StrategyParam<bool> _useAggressiveEntries;
private readonly StrategyParam<bool> _tradeReverse;
private readonly StrategyParam<bool> _oneOrderAtATime;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<bool> _useDynamicVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<IndicatorSources> _indicatorChoice;
private readonly StrategyParam<int> _indicatorTopValue;
private readonly StrategyParam<int> _indicatorBottomValue;
private readonly StrategyParam<int> _indicatorPeriod;
private readonly StrategyParam<bool> _useDynamicTargets;
private readonly StrategyParam<int> _staticStopLossPoints;
private readonly StrategyParam<int> _staticTakeProfitPoints;
private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
private readonly StrategyParam<decimal> _takeProfitAtrMultiplier;
private readonly StrategyParam<bool> _useBreakEven;
private readonly StrategyParam<int> _breakEvenTriggerPoints;
private readonly StrategyParam<int> _breakEvenPaddingPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal indicator, decimal close)> _history = new();
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private IIndicator _indicator;
private AverageTrueRange _atr;
/// <summary>
/// Indicator source used for optimization.
/// </summary>
public enum IndicatorSources
{
/// <summary>
/// Use Relative Strength Index values.
/// </summary>
RelativeStrengthIndex,
/// <summary>
/// Use Money Flow Index values.
/// </summary>
MoneyFlowIndex,
}
/// <summary>
/// Number of bars evaluated when searching for best thresholds.
/// </summary>
public int OptimizingPeriods
{
get => _optimizingPeriods.Value;
set => _optimizingPeriods.Value = value;
}
/// <summary>
/// Allow entries without waiting for indicator crosses.
/// </summary>
public bool UseAggressiveEntries
{
get => _useAggressiveEntries.Value;
set => _useAggressiveEntries.Value = value;
}
/// <summary>
/// Invert profitability preference to trade opposite direction.
/// </summary>
public bool TradeReverse
{
get => _tradeReverse.Value;
set => _tradeReverse.Value = value;
}
/// <summary>
/// Restrict strategy to a single open position at a time.
/// </summary>
public bool OneOrderAtATime
{
get => _oneOrderAtATime.Value;
set => _oneOrderAtATime.Value = value;
}
/// <summary>
/// Static volume used when dynamic sizing is disabled.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Enable risk based position sizing.
/// </summary>
public bool UseDynamicVolume
{
get => _useDynamicVolume.Value;
set => _useDynamicVolume.Value = value;
}
/// <summary>
/// Percentage of portfolio risked per trade when sizing dynamically.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Oscillator used for optimization.
/// </summary>
public IndicatorSources IndicatorChoice
{
get => _indicatorChoice.Value;
set => _indicatorChoice.Value = value;
}
/// <summary>
/// Highest threshold tested when searching for overbought levels.
/// </summary>
public int IndicatorTopValue
{
get => _indicatorTopValue.Value;
set => _indicatorTopValue.Value = value;
}
/// <summary>
/// Lowest threshold tested when searching for oversold levels.
/// </summary>
public int IndicatorBottomValue
{
get => _indicatorBottomValue.Value;
set => _indicatorBottomValue.Value = value;
}
/// <summary>
/// Period used for the selected indicator.
/// </summary>
public int IndicatorPeriod
{
get => _indicatorPeriod.Value;
set => _indicatorPeriod.Value = value;
}
/// <summary>
/// Enable ATR based stop-loss and take-profit levels.
/// </summary>
public bool UseDynamicTargets
{
get => _useDynamicTargets.Value;
set => _useDynamicTargets.Value = value;
}
/// <summary>
/// Static stop-loss distance expressed in points when dynamic targets are disabled.
/// </summary>
public int StaticStopLossPoints
{
get => _staticStopLossPoints.Value;
set => _staticStopLossPoints.Value = value;
}
/// <summary>
/// Static take-profit distance expressed in points when dynamic targets are disabled.
/// </summary>
public int StaticTakeProfitPoints
{
get => _staticTakeProfitPoints.Value;
set => _staticTakeProfitPoints.Value = value;
}
/// <summary>
/// ATR multiplier applied to stop-loss when dynamic targets are enabled.
/// </summary>
public decimal StopLossAtrMultiplier
{
get => _stopLossAtrMultiplier.Value;
set => _stopLossAtrMultiplier.Value = value;
}
/// <summary>
/// ATR multiplier applied to take-profit when dynamic targets are enabled.
/// </summary>
public decimal TakeProfitAtrMultiplier
{
get => _takeProfitAtrMultiplier.Value;
set => _takeProfitAtrMultiplier.Value = value;
}
/// <summary>
/// Enable stop adjustment to breakeven once profit target is reached.
/// </summary>
public bool UseBreakEven
{
get => _useBreakEven.Value;
set => _useBreakEven.Value = value;
}
/// <summary>
/// Profit threshold in points required to arm the breakeven stop.
/// </summary>
public int BreakEvenTriggerPoints
{
get => _breakEvenTriggerPoints.Value;
set => _breakEvenTriggerPoints.Value = value;
}
/// <summary>
/// Additional padding in points applied once breakeven triggers.
/// </summary>
public int BreakEvenPaddingPoints
{
get => _breakEvenPaddingPoints.Value;
set => _breakEvenPaddingPoints.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="SelfOptimizingRsiOrMfiTraderV3Strategy"/>.
/// </summary>
public SelfOptimizingRsiOrMfiTraderV3Strategy()
{
_optimizingPeriods = Param(nameof(OptimizingPeriods), 30)
.SetGreaterThanZero()
.SetDisplay("Optimization Bars", "Number of bars used for optimization", "General")
.SetOptimize(20, 100, 10);
_useAggressiveEntries = Param(nameof(UseAggressiveEntries), false)
.SetDisplay("Aggressive Entries", "Allow entries without indicator crosses", "Trading");
_tradeReverse = Param(nameof(TradeReverse), false)
.SetDisplay("Reverse Trading", "Swap profitability preference for opposite trades", "Trading");
_oneOrderAtATime = Param(nameof(OneOrderAtATime), true)
.SetDisplay("One Position", "Permit only one open position", "Trading");
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Static order volume when sizing manually", "Risk");
_useDynamicVolume = Param(nameof(UseDynamicVolume), true)
.SetDisplay("Dynamic Volume", "Use risk percentage for position sizing", "Risk");
_riskPercent = Param(nameof(RiskPercent), 2m)
.SetRange(0.1m, 10m)
.SetDisplay("Risk %", "Percent of capital risked per trade", "Risk");
_indicatorChoice = Param(nameof(IndicatorChoice), IndicatorSources.RelativeStrengthIndex)
.SetDisplay("Indicator", "Oscillator optimized by the strategy", "Indicator");
_indicatorTopValue = Param(nameof(IndicatorTopValue), 100)
.SetDisplay("Top Level", "Upper bound for level search", "Indicator");
_indicatorBottomValue = Param(nameof(IndicatorBottomValue), 0)
.SetDisplay("Bottom Level", "Lower bound for level search", "Indicator");
_indicatorPeriod = Param(nameof(IndicatorPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Indicator Period", "Averaging period for RSI or MFI", "Indicator");
_useDynamicTargets = Param(nameof(UseDynamicTargets), true)
.SetDisplay("Dynamic Targets", "Use ATR based stop-loss and take-profit", "Risk");
_staticStopLossPoints = Param(nameof(StaticStopLossPoints), 1000)
.SetGreaterThanZero()
.SetDisplay("Static Stop", "Stop-loss in points when dynamic targets disabled", "Risk");
_staticTakeProfitPoints = Param(nameof(StaticTakeProfitPoints), 2000)
.SetGreaterThanZero()
.SetDisplay("Static Take", "Take-profit in points when dynamic targets disabled", "Risk");
_stopLossAtrMultiplier = Param(nameof(StopLossAtrMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("ATR Stop Mult", "Stop-loss multiplier applied to ATR", "Risk");
_takeProfitAtrMultiplier = Param(nameof(TakeProfitAtrMultiplier), 7m)
.SetGreaterThanZero()
.SetDisplay("ATR Take Mult", "Take-profit multiplier applied to ATR", "Risk");
_useBreakEven = Param(nameof(UseBreakEven), true)
.SetDisplay("Use Breakeven", "Move stop to breakeven after trigger", "Risk");
_breakEvenTriggerPoints = Param(nameof(BreakEvenTriggerPoints), 200)
.SetGreaterThanZero()
.SetDisplay("Breakeven Trigger", "Profit in points required to arm breakeven", "Risk");
_breakEvenPaddingPoints = Param(nameof(BreakEvenPaddingPoints), 100)
.SetGreaterThanZero()
.SetDisplay("Breakeven Padding", "Padding in points applied after trigger", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for analysis", "General");
Volume = 1m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
_indicator = null;
_atr = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_indicator = IndicatorChoice switch
{
IndicatorSources.MoneyFlowIndex => new MoneyFlowIndex { Length = IndicatorPeriod },
_ => new RelativeStrengthIndex { Length = IndicatorPeriod }
};
_atr = new AverageTrueRange { Length = IndicatorPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_indicator, _atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _indicator);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal indicatorValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
_history.Add((indicatorValue, candle.ClosePrice));
var maxNeeded = Math.Max(OptimizingPeriods + 1, 3);
while (_history.Count > maxNeeded)
{
_history.RemoveAt(0);
}
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0m)
priceStep = 1m;
var stepPrice = priceStep;
var triggerDiff = UseBreakEven ? BreakEvenTriggerPoints * priceStep : 0m;
var paddingPoints = BreakEvenPaddingPoints > BreakEvenTriggerPoints ? 0 : BreakEvenPaddingPoints;
var paddingDiff = UseBreakEven ? paddingPoints * priceStep : 0m;
ManageOpenPosition(candle, triggerDiff, paddingDiff);
if (_history.Count < maxNeeded)
return;
var indicatorValues = new decimal[_history.Count];
var closeValues = new decimal[_history.Count];
for (var i = 0; i < _history.Count; i++)
{
var source = _history[_history.Count - 1 - i];
indicatorValues[i] = source.indicator;
closeValues[i] = source.close;
}
decimal stopLossDiff;
decimal takeProfitDiff;
if (UseDynamicTargets)
{
if (atrValue <= 0m)
return;
stopLossDiff = atrValue * StopLossAtrMultiplier;
takeProfitDiff = atrValue * TakeProfitAtrMultiplier;
}
else
{
stopLossDiff = StaticStopLossPoints * priceStep;
takeProfitDiff = StaticTakeProfitPoints * priceStep;
}
if (stopLossDiff <= 0m || takeProfitDiff <= 0m)
return;
var volume = CalculateVolume(stopLossDiff);
if (volume <= 0m)
return;
var stepMultiplier = priceStep > 0m ? stepPrice / priceStep : 1m;
var (sellLevel, sellProfit) = CalculateBestSellLevel(indicatorValues, closeValues, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
var (buyLevel, buyProfit) = CalculateBestBuyLevel(indicatorValues, closeValues, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
var adjustedSellProfit = sellProfit;
var adjustedBuyProfit = buyProfit;
if (TradeReverse)
{
adjustedSellProfit = buyProfit;
adjustedBuyProfit = sellProfit;
}
var canEnter = !OneOrderAtATime || Position == 0m;
var currentIndicator = indicatorValues[0];
var previousIndicator = indicatorValues[1];
if (adjustedSellProfit > adjustedBuyProfit)
{
if (canEnter && ((currentIndicator < sellLevel && previousIndicator > sellLevel) || UseAggressiveEntries))
{
EnterShort(candle, volume, stopLossDiff, takeProfitDiff);
}
}
else if (adjustedSellProfit < adjustedBuyProfit)
{
if (canEnter && ((currentIndicator > buyLevel && previousIndicator < buyLevel) || UseAggressiveEntries))
{
EnterLong(candle, volume, stopLossDiff, takeProfitDiff);
}
}
}
private (int level, decimal profit) CalculateBestSellLevel(decimal[] indicatorValues, decimal[] closeValues, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
{
var bottom = Math.Min(IndicatorBottomValue, IndicatorTopValue);
var top = Math.Max(IndicatorBottomValue, IndicatorTopValue);
var bestProfit = 0m;
var bestLevel = bottom;
var updated = false;
for (var level = bottom; level <= top; level++)
{
var profit = EvaluateSellLevel(indicatorValues, closeValues, level, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
if (profit > bestProfit)
{
bestProfit = profit;
bestLevel = level;
updated = true;
}
}
return (bestLevel, updated ? bestProfit : 0m);
}
private (int level, decimal profit) CalculateBestBuyLevel(decimal[] indicatorValues, decimal[] closeValues, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
{
var bottom = Math.Min(IndicatorBottomValue, IndicatorTopValue);
var top = Math.Max(IndicatorBottomValue, IndicatorTopValue);
var bestProfit = 0m;
var bestLevel = top;
var updated = false;
for (var level = top; level >= bottom; level--)
{
var profit = EvaluateBuyLevel(indicatorValues, closeValues, level, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
if (profit > bestProfit)
{
bestProfit = profit;
bestLevel = level;
updated = true;
}
}
return (bestLevel, updated ? bestProfit : 0m);
}
private decimal EvaluateSellLevel(decimal[] indicatorValues, decimal[] closeValues, int level, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
{
var totalProfit = 0m;
if (indicatorValues.Length < 3)
return 0m;
var threshold = (decimal)level;
for (var i = indicatorValues.Length - 2; i >= 2; i--)
{
if (indicatorValues[i] < threshold && indicatorValues[i + 1] > threshold)
{
var entryPrice = closeValues[i];
for (var j = i - 1; j >= 1; j--)
{
var price = closeValues[j];
if (price >= entryPrice + stopLossDiff)
{
var loss = (price - entryPrice) * stepMultiplier * volume;
totalProfit -= loss;
i = j;
break;
}
if (price <= entryPrice - takeProfitDiff)
{
var gain = (entryPrice - price) * stepMultiplier * volume;
totalProfit += gain;
i = j;
break;
}
}
}
}
return totalProfit;
}
private decimal EvaluateBuyLevel(decimal[] indicatorValues, decimal[] closeValues, int level, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
{
var totalProfit = 0m;
if (indicatorValues.Length < 3)
return 0m;
var threshold = (decimal)level;
for (var i = indicatorValues.Length - 2; i >= 2; i--)
{
if (indicatorValues[i] > threshold && indicatorValues[i + 1] < threshold)
{
var entryPrice = closeValues[i];
for (var j = i - 1; j >= 1; j--)
{
var price = closeValues[j];
if (price <= entryPrice - stopLossDiff)
{
var loss = (entryPrice - price) * stepMultiplier * volume;
totalProfit -= loss;
i = j;
break;
}
if (price >= entryPrice + takeProfitDiff)
{
var gain = (price - entryPrice) * stepMultiplier * volume;
totalProfit += gain;
i = j;
break;
}
}
}
}
return totalProfit;
}
private decimal CalculateVolume(decimal stopLossDiff)
{
var volume = BaseVolume;
if (UseDynamicVolume && stopLossDiff > 0m && Security != null)
{
var priceStep = Security.PriceStep ?? 0m;
var stepPrice = priceStep;
if (priceStep > 0m && stepPrice > 0m)
{
var stopPoints = stopLossDiff / priceStep;
var riskPerUnit = stopPoints * stepPrice;
var capital = Portfolio?.CurrentValue ?? 0m;
var riskBudget = capital * (RiskPercent / 100m);
if (riskPerUnit > 0m && riskBudget > 0m)
{
var rawVolume = riskBudget / riskPerUnit;
if (rawVolume > 0m)
volume = rawVolume;
}
}
}
return AdjustVolume(volume);
}
private decimal AdjustVolume(decimal volume)
{
if (Security == null)
return Math.Max(volume, 0.01m);
var step = Security.VolumeStep ?? 0m;
var min = Security.MinVolume ?? 0m;
var max = Security.MaxVolume ?? decimal.MaxValue;
if (step <= 0m)
step = 1m;
if (min <= 0m)
min = step;
if (volume < min)
volume = min;
if (volume > max)
volume = max;
volume = Math.Floor(volume / step) * step;
if (volume <= 0m)
volume = min;
return volume;
}
private void EnterLong(ICandleMessage candle, decimal volume, decimal stopLossDiff, decimal takeProfitDiff)
{
var orderVolume = volume;
if (Position < 0m)
orderVolume += Math.Abs(Position);
BuyMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - stopLossDiff;
_takeProfitPrice = _entryPrice + takeProfitDiff;
}
private void EnterShort(ICandleMessage candle, decimal volume, decimal stopLossDiff, decimal takeProfitDiff)
{
var orderVolume = volume;
if (Position > 0m)
orderVolume += Position;
SellMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + stopLossDiff;
_takeProfitPrice = _entryPrice - takeProfitDiff;
}
private void ManageOpenPosition(ICandleMessage candle, decimal triggerDiff, decimal paddingDiff)
{
if (Position > 0m)
{
if (UseBreakEven && _entryPrice is decimal entry && _stopPrice is decimal currentStop)
{
var triggerPrice = entry + triggerDiff;
var targetStop = entry + paddingDiff;
if (triggerDiff > 0m && candle.HighPrice >= triggerPrice && currentStop < targetStop)
_stopPrice = targetStop;
}
if (_stopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket();
ResetPositionState();
return;
}
if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
{
SellMarket();
ResetPositionState();
return;
}
}
else if (Position < 0m)
{
if (UseBreakEven && _entryPrice is decimal entry && _stopPrice is decimal currentStop)
{
var triggerPrice = entry - triggerDiff;
var targetStop = entry - paddingDiff;
if (triggerDiff > 0m && candle.LowPrice <= triggerPrice && currentStop > targetStop)
_stopPrice = targetStop;
}
if (_stopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket();
ResetPositionState();
return;
}
if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
{
BuyMarket();
ResetPositionState();
return;
}
}
else
{
ResetPositionState();
}
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
RelativeStrengthIndex,
MoneyFlowIndex,
AverageTrueRange,
)
class self_optimizing_rsi_or_mfi_trader_v3_strategy(Strategy):
"""Self-optimizing RSI/MFI: dynamically finds best overbought/oversold levels over rolling history."""
def __init__(self):
super(self_optimizing_rsi_or_mfi_trader_v3_strategy, self).__init__()
self._optimizing_periods = self.Param("OptimizingPeriods", 30) \
.SetGreaterThanZero() \
.SetDisplay("Optimization Bars", "Number of bars used for optimization", "General")
self._use_aggressive_entries = self.Param("UseAggressiveEntries", False) \
.SetDisplay("Aggressive Entries", "Allow entries without indicator crosses", "Trading")
self._trade_reverse = self.Param("TradeReverse", False) \
.SetDisplay("Reverse Trading", "Swap profitability preference for opposite trades", "Trading")
self._one_order_at_a_time = self.Param("OneOrderAtATime", True) \
.SetDisplay("One Position", "Permit only one open position", "Trading")
self._base_volume = self.Param("BaseVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Base Volume", "Static order volume when sizing manually", "Risk")
self._use_dynamic_volume = self.Param("UseDynamicVolume", True) \
.SetDisplay("Dynamic Volume", "Use risk percentage for position sizing", "Risk")
self._risk_percent = self.Param("RiskPercent", 2.0) \
.SetDisplay("Risk %", "Percent of capital risked per trade", "Risk")
# 0=RSI, 1=MFI
self._indicator_choice = self.Param("IndicatorChoice", 0) \
.SetDisplay("Indicator", "0=RSI, 1=MFI", "Indicator")
self._indicator_top_value = self.Param("IndicatorTopValue", 100) \
.SetDisplay("Top Level", "Upper bound for level search", "Indicator")
self._indicator_bottom_value = self.Param("IndicatorBottomValue", 0) \
.SetDisplay("Bottom Level", "Lower bound for level search", "Indicator")
self._indicator_period = self.Param("IndicatorPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("Indicator Period", "Averaging period for RSI or MFI", "Indicator")
self._use_dynamic_targets = self.Param("UseDynamicTargets", True) \
.SetDisplay("Dynamic Targets", "Use ATR based stop-loss and take-profit", "Risk")
self._static_stop_loss_points = self.Param("StaticStopLossPoints", 1000) \
.SetGreaterThanZero() \
.SetDisplay("Static Stop", "Stop-loss in points when dynamic targets disabled", "Risk")
self._static_take_profit_points = self.Param("StaticTakeProfitPoints", 2000) \
.SetGreaterThanZero() \
.SetDisplay("Static Take", "Take-profit in points when dynamic targets disabled", "Risk")
self._stop_loss_atr_multiplier = self.Param("StopLossAtrMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR Stop Mult", "Stop-loss multiplier applied to ATR", "Risk")
self._take_profit_atr_multiplier = self.Param("TakeProfitAtrMultiplier", 7.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR Take Mult", "Take-profit multiplier applied to ATR", "Risk")
self._use_break_even = self.Param("UseBreakEven", True) \
.SetDisplay("Use Breakeven", "Move stop to breakeven after trigger", "Risk")
self._break_even_trigger_points = self.Param("BreakEvenTriggerPoints", 200) \
.SetGreaterThanZero() \
.SetDisplay("Breakeven Trigger", "Profit in points required to arm breakeven", "Risk")
self._break_even_padding_points = self.Param("BreakEvenPaddingPoints", 100) \
.SetGreaterThanZero() \
.SetDisplay("Breakeven Padding", "Padding in points applied after trigger", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe used for analysis", "General")
self._history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
@property
def OptimizingPeriods(self):
return int(self._optimizing_periods.Value)
@property
def UseAggressiveEntries(self):
return self._use_aggressive_entries.Value
@property
def TradeReverse(self):
return self._trade_reverse.Value
@property
def OneOrderAtATime(self):
return self._one_order_at_a_time.Value
@property
def BaseVolume(self):
return float(self._base_volume.Value)
@property
def UseDynamicVolume(self):
return self._use_dynamic_volume.Value
@property
def RiskPercent(self):
return float(self._risk_percent.Value)
@property
def IndicatorChoice(self):
return int(self._indicator_choice.Value)
@property
def IndicatorTopValue(self):
return int(self._indicator_top_value.Value)
@property
def IndicatorBottomValue(self):
return int(self._indicator_bottom_value.Value)
@property
def IndicatorPeriod(self):
return int(self._indicator_period.Value)
@property
def UseDynamicTargets(self):
return self._use_dynamic_targets.Value
@property
def StaticStopLossPoints(self):
return int(self._static_stop_loss_points.Value)
@property
def StaticTakeProfitPoints(self):
return int(self._static_take_profit_points.Value)
@property
def StopLossAtrMultiplier(self):
return float(self._stop_loss_atr_multiplier.Value)
@property
def TakeProfitAtrMultiplier(self):
return float(self._take_profit_atr_multiplier.Value)
@property
def UseBreakEven(self):
return self._use_break_even.Value
@property
def BreakEvenTriggerPoints(self):
return int(self._break_even_trigger_points.Value)
@property
def BreakEvenPaddingPoints(self):
return int(self._break_even_padding_points.Value)
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(self_optimizing_rsi_or_mfi_trader_v3_strategy, self).OnStarted2(time)
self._history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
if self.IndicatorChoice == 1:
self._indicator = MoneyFlowIndex()
else:
self._indicator = RelativeStrengthIndex()
self._indicator.Length = self.IndicatorPeriod
self._atr = AverageTrueRange()
self._atr.Length = self.IndicatorPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._indicator, self._atr, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._indicator)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
def process_candle(self, candle, indicator_value, atr_value):
if candle.State != CandleStates.Finished:
return
ind_val = float(indicator_value)
atr_val = float(atr_value)
close = float(candle.ClosePrice)
self._history.append((ind_val, close))
max_needed = max(self.OptimizingPeriods + 1, 3)
while len(self._history) > max_needed:
self._history.pop(0)
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
if price_step <= 0:
price_step = 1.0
trigger_diff = self.BreakEvenTriggerPoints * price_step if self.UseBreakEven else 0.0
padding_pts = self.BreakEvenPaddingPoints if self.BreakEvenPaddingPoints <= self.BreakEvenTriggerPoints else 0
padding_diff = padding_pts * price_step if self.UseBreakEven else 0.0
self._manage_open_position(candle, trigger_diff, padding_diff)
if len(self._history) < max_needed:
return
# Build arrays (newest first)
indicator_values = []
close_values = []
for i in range(len(self._history) - 1, -1, -1):
indicator_values.append(self._history[i][0])
close_values.append(self._history[i][1])
if self.UseDynamicTargets:
if atr_val <= 0:
return
stop_loss_diff = atr_val * self.StopLossAtrMultiplier
take_profit_diff = atr_val * self.TakeProfitAtrMultiplier
else:
stop_loss_diff = self.StaticStopLossPoints * price_step
take_profit_diff = self.StaticTakeProfitPoints * price_step
if stop_loss_diff <= 0 or take_profit_diff <= 0:
return
volume = self.BaseVolume
step_multiplier = 1.0
sell_level, sell_profit = self._calc_best_sell_level(indicator_values, close_values, stop_loss_diff, take_profit_diff, volume, step_multiplier)
buy_level, buy_profit = self._calc_best_buy_level(indicator_values, close_values, stop_loss_diff, take_profit_diff, volume, step_multiplier)
adjusted_sell = sell_profit
adjusted_buy = buy_profit
if self.TradeReverse:
adjusted_sell = buy_profit
adjusted_buy = sell_profit
can_enter = not self.OneOrderAtATime or self.Position == 0
current_ind = indicator_values[0]
prev_ind = indicator_values[1]
if adjusted_sell > adjusted_buy:
if can_enter and ((current_ind < sell_level and prev_ind > sell_level) or self.UseAggressiveEntries):
self._enter_short(candle, stop_loss_diff, take_profit_diff)
elif adjusted_sell < adjusted_buy:
if can_enter and ((current_ind > buy_level and prev_ind < buy_level) or self.UseAggressiveEntries):
self._enter_long(candle, stop_loss_diff, take_profit_diff)
def _calc_best_sell_level(self, ind_vals, close_vals, sl_diff, tp_diff, volume, step_mult):
bottom = min(self.IndicatorBottomValue, self.IndicatorTopValue)
top = max(self.IndicatorBottomValue, self.IndicatorTopValue)
best_profit = 0.0
best_level = bottom
updated = False
for level in range(bottom, top + 1):
profit = self._eval_sell(ind_vals, close_vals, level, sl_diff, tp_diff, volume, step_mult)
if profit > best_profit:
best_profit = profit
best_level = level
updated = True
return (best_level, best_profit if updated else 0.0)
def _calc_best_buy_level(self, ind_vals, close_vals, sl_diff, tp_diff, volume, step_mult):
bottom = min(self.IndicatorBottomValue, self.IndicatorTopValue)
top = max(self.IndicatorBottomValue, self.IndicatorTopValue)
best_profit = 0.0
best_level = top
updated = False
for level in range(top, bottom - 1, -1):
profit = self._eval_buy(ind_vals, close_vals, level, sl_diff, tp_diff, volume, step_mult)
if profit > best_profit:
best_profit = profit
best_level = level
updated = True
return (best_level, best_profit if updated else 0.0)
def _eval_sell(self, ind_vals, close_vals, level, sl_diff, tp_diff, volume, step_mult):
total = 0.0
n = len(ind_vals)
if n < 3:
return 0.0
threshold = float(level)
i = n - 2
while i >= 2:
if ind_vals[i] < threshold and ind_vals[i + 1] > threshold:
entry = close_vals[i]
j = i - 1
while j >= 1:
price = close_vals[j]
if price >= entry + sl_diff:
total -= (price - entry) * step_mult * volume
i = j
break
if price <= entry - tp_diff:
total += (entry - price) * step_mult * volume
i = j
break
j -= 1
i -= 1
return total
def _eval_buy(self, ind_vals, close_vals, level, sl_diff, tp_diff, volume, step_mult):
total = 0.0
n = len(ind_vals)
if n < 3:
return 0.0
threshold = float(level)
i = n - 2
while i >= 2:
if ind_vals[i] > threshold and ind_vals[i + 1] < threshold:
entry = close_vals[i]
j = i - 1
while j >= 1:
price = close_vals[j]
if price <= entry - sl_diff:
total -= (entry - price) * step_mult * volume
i = j
break
if price >= entry + tp_diff:
total += (price - entry) * step_mult * volume
i = j
break
j -= 1
i -= 1
return total
def _enter_long(self, candle, sl_diff, tp_diff):
self.BuyMarket()
self._entry_price = float(candle.ClosePrice)
self._stop_price = self._entry_price - sl_diff
self._take_profit_price = self._entry_price + tp_diff
def _enter_short(self, candle, sl_diff, tp_diff):
self.SellMarket()
self._entry_price = float(candle.ClosePrice)
self._stop_price = self._entry_price + sl_diff
self._take_profit_price = self._entry_price - tp_diff
def _manage_open_position(self, candle, trigger_diff, padding_diff):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self.Position > 0:
if self.UseBreakEven and self._entry_price is not None and self._stop_price is not None:
trigger_price = self._entry_price + trigger_diff
target_stop = self._entry_price + padding_diff
if trigger_diff > 0 and h >= trigger_price and self._stop_price < target_stop:
self._stop_price = target_stop
if self._stop_price is not None and lo <= self._stop_price:
self.SellMarket()
self._reset_position_state()
return
if self._take_profit_price is not None and h >= self._take_profit_price:
self.SellMarket()
self._reset_position_state()
return
elif self.Position < 0:
if self.UseBreakEven and self._entry_price is not None and self._stop_price is not None:
trigger_price = self._entry_price - trigger_diff
target_stop = self._entry_price - padding_diff
if trigger_diff > 0 and lo <= trigger_price and self._stop_price > target_stop:
self._stop_price = target_stop
if self._stop_price is not None and h >= self._stop_price:
self.BuyMarket()
self._reset_position_state()
return
if self._take_profit_price is not None and lo <= self._take_profit_price:
self.BuyMarket()
self._reset_position_state()
return
else:
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def OnReseted(self):
super(self_optimizing_rsi_or_mfi_trader_v3_strategy, self).OnReseted()
self._history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def CreateClone(self):
return self_optimizing_rsi_or_mfi_trader_v3_strategy()