自优化 RSI / MFI 交易策略 v3
概述
该策略将 MetaTrader 中的 "Self Optimizing RSI or MFI Trader" 专家顾问迁移到 StockSharp 高阶 API。每根已完成的 K 线都会对过去 OptimizingPeriods 根历史数据进行回测,寻找在该窗口内表现最优的超买与超卖阈值。当实时的指标数值向最佳阈值方向发生交叉(或者在启用 UseAggressiveEntries 时无需等待交叉)时,策略按照历史表现较优的方向开仓。仓位的止损、止盈可以使用 ATR 倍数动态计算,也可以使用固定点差,并可在达到一定浮盈后自动移至保本。
行情数据
- 适用于任何提供 OHLC 蜡烛图的品种;如选择 MFI,需要成交量数据。
- 使用
CandleType参数指定的时间框架,默认采用 15 分钟 K 线,可根据接入的交易所选择其它周期。
指标
- 根据
IndicatorChoice参数选择 RSI 或 MFI,两者共用同一周期长度。 - 启用
UseDynamicTargets时使用 ATR 计算动态止损与止盈距离。
交易逻辑
- 维护最近
OptimizingPeriods + 1根已完成 K 线的指标值与收盘价。 - 在
IndicatorBottomValue与IndicatorTopValue之间遍历每一个整数阈值:- 对于做空情景,统计指标从上向下穿越该阈值的次数,并判断假设的止损或止盈谁先触发。
- 对于做多情景,统计指标从下向上穿越该阈值时的收益表现。
- 选取在回测窗口内带来最大模拟收益的阈值。若启用
TradeReverse,则交换多空方向的收益评分以执行反向交易。 - 当实时指标跨越最优阈值且方向与历史优势一致时(或开启激进模式时立刻),并满足
OneOrderAtATime的限制后开仓。 - 仓位管理:
- 动态模式下使用 ATR ×
StopLossAtrMultiplier/TakeProfitAtrMultiplier得出价格距离;静态模式下使用StaticStopLossPoints/StaticTakeProfitPoints与品种的最小跳动点计算出价格。 - 若启用
UseBreakEven,在浮盈达到BreakEvenTriggerPoints时将止损上移/下移至入场价并加上BreakEvenPaddingPoints的缓冲。 - 当价格触及止损或止盈水平时立即平仓。
- 动态模式下使用 ATR ×
风险控制
- 动态仓位: 启用
UseDynamicVolume时按照RiskPercent的组合价值来计算开仓数量,通过品种的PriceStep与StepPrice将止损距离换算成货币风险。 - 固定仓位: 关闭动态仓位时,每次按
BaseVolume交易。 - 保本移动: 防止盈利头寸在达到既定浮盈后回吐。
参数说明
| 参数 | 说明 |
|---|---|
OptimizingPeriods |
滑动优化窗口内的历史 K 线数量(默认 144)。 |
IndicatorChoice |
选择 RSI 或 MFI 作为信号指标。 |
IndicatorPeriod |
指标与 ATR 的计算周期。 |
IndicatorTopValue / IndicatorBottomValue |
搜索阈值的上下限(通常为 0–100)。 |
UseAggressiveEntries |
启用后无需等待交叉即可入场。 |
TradeReverse |
交换历史收益评分,转而交易另一方向。 |
OneOrderAtATime |
控制是否同一时间仅允许一个净头寸。 |
UseDynamicTargets |
切换 ATR 动态止损/止盈或固定点差。 |
StopLossAtrMultiplier, TakeProfitAtrMultiplier |
动态模式下的 ATR 倍数。 |
StaticStopLossPoints, StaticTakeProfitPoints |
静态模式下的点数距离。 |
UseBreakEven, BreakEvenTriggerPoints, BreakEvenPaddingPoints |
保本移动的触发与缓冲设置。 |
UseDynamicVolume, RiskPercent, BaseVolume |
仓位管理设置。 |
CandleType |
交易与优化所使用的时间框架。 |
实现细节
- 采用
SubscribeCandles().Bind(...)链路,仅在蜡烛完成后运行逻辑。 - 在净持仓账户中建议保持
OneOrderAtATime=true,因为实现仅跟踪单个聚合持仓。 - ATR 模式需要等待指标形成后才开始交易,否则会跳过信号。
- 选择 MFI 时必须有成交量,否则指标值为零导致无法下单。
优化建议
- 同时优化
OptimizingPeriods、IndicatorPeriod以及 ATR 倍数,使其适应不同品种的波动特征。 - 对于震荡较小的品种,可以缩小阈值搜索范围(如 20–80)。
- 建议在实盘前进行走步式前向测试,以验证自适应阈值在不同阶段的稳健性。
使用步骤
- 在 Designer 或代码中实例化策略,设置交易账户与标的。
- 调整参数、止损止盈以及仓位规则。
- 启动策略,积累足够历史数据后将自动开始交易。
限制
- 每根 K 线都要进行阈值优化,若窗口过大或范围过宽可能造成 CPU 压力。
- 阈值仅遍历整数,不会测试 70.5 等小数阈值。
- 策略假设近期历史具有延续性,若市场快速换挡需及时调整参数或停止策略。
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()