True Scalper Profit Lock 策略
概述
True Scalper Profit Lock 策略 是 MetaTrader 5 智能交易系统 “True Scalper Profit Lock” 的 StockSharp 移植版本。策略以超短线交易为目标,结合快速指数移动平均线、2 周期 RSI 过滤器以及将止损推向保本位置的利润保护机制。当持仓在设定的 K 线数量内没有达到目标时,“弃单” 模块会强制离场。
实现代码仅订阅一个 K 线数据流,并且只在 K 线收盘后做出决策。默认适用于日内剥头皮场景,但所有参数都可以调整,从而适配不同品种与周期。
指标与数据
- EMA(快线) – 默认周期为 3,用于识别向上的动量。
- EMA(慢线) – 默认周期为 7,定义短期趋势方向。
- RSI – 默认周期为 2,提供两种判定模式:
- Method A(默认关闭):仅在 RSI 穿越阈值时改变信号方向。
- Method B(默认开启):根据 RSI 位于阈值之上或之下决定信号方向。
- K 线 – 默认时间框架为 1 分钟,可通过
CandleType参数配置。
入场逻辑
- 在最新收盘 K 线上计算快慢 EMA 与 RSI。
- 根据所选模式确定 RSI 极性:
- Method A:比较当前值与上一根 K 线的值,识别阈值穿越。
- Method B:直接判断当前 RSI 是否高于或低于阈值。
- 做多条件 – 当快 EMA 至少高于慢 EMA 一个最小报价步长,并且 RSI 极性为 “负” 时入场;若弃单逻辑要求反向做多,也会忽略当前信号直接建仓。
- 做空条件 – 当快 EMA 至少低于慢 EMA 一个最小报价步长,并且 RSI 极性为 “正” 时入场;弃单反向信号同样会触发做空。
- 反手时,使用一笔市场单在同一时间平掉旧仓并建立新仓。
离场逻辑
- 止损 / 止盈 – 以
StopLossPoints、TakeProfitPoints指定的价格步长距离在入场后立即生效。 - 利润锁定 – 启用时,持仓盈利达到
BreakEvenTriggerPoints指定的步长后,将止损移动到入场价再加上BreakEvenPoints的安全垫(做空时为入场价减去该距离)。每笔交易只执行一次。 - 弃单逻辑 – 根据入场后经历的收盘 K 线数量触发:
- Method A:达到
AbandonBars后平仓,并在下一次机会强制反向建仓。 - Method B:达到阈值后仅平仓,后续方向继续依赖信号。
- 当两种方法同时开启时,Method A 拥有优先级。
- Method A:达到
- 所有离场均使用市场单(
ClosePosition)完成,并在执行后重置内部状态。
资金管理
- 启用
UseMoneyManagement时,按照原始 EA 的公式动态计算手数:Ceiling(Balance * RiskPercent / 10000) / 10。 - 计算结果会遵守 MT5 版的边界条件:低于 0.1 手时回落到
InitialVolume,大于 1 手时向上取整,迷你账户可乘以 10,总手数上限为 100。 - 关闭资金管理时始终使用固定的
InitialVolume。
参数说明
InitialVolume– 关闭资金管理时的基础手数。TakeProfitPoints/StopLossPoints– 以Security.PriceStep表示的止盈、止损距离。FastPeriod、SlowPeriod、RsiLength、RsiThreshold– 指标配置。UseRsiMethodA、UseRsiMethodB– 选择 RSI 判定逻辑。UseAbandonMethodA、UseAbandonMethodB、AbandonBars– 弃单模块设置。UseMoneyManagement、RiskPercent、LiveTrading、IsMiniAccount– 资金管理相关选项,与原 EA 保持一致。UseProfitLock、BreakEvenTriggerPoints、BreakEvenPoints– 保本移动参数。MaxPositions– 为兼容 MQL 版本保留。当前移植采用净仓制度,仍然一次只管理一个净头寸。CandleType– 信号所使用的周期或自定义 K 线类型。
使用提示
- 策略只需绑定一个交易品种,
GetWorkingSecurities会自动订阅所选的 K 线类型。 - 利润锁定与弃单逻辑依赖收盘价,因此同一根 K 线内的瞬时穿越不会触发。
- 原 MT5 参数
Slippage在源码中未被使用,因此移植版本未包含该设置。 - 请根据标的的最小报价步长调节相关参数,以维持原本的点差距离。
转换差异
- StockSharp 采用净仓模式,即便
MaxPositions大于 1 也不会同时开启多笔同向持仓,这与原 EA 在maxTradesPerPair = 1时的表现一致。 - 订单管理改用
BuyMarket、SellMarket、ClosePosition等高阶 API,不再直接操作交易票据。 - 指标数据通过
Bind回调传入,无需手动访问指标缓冲区。
测试建议
- 在与原 EA 相同的时间框架(推荐 1 分钟)上进行历史回测,验证行为是否一致。
- 针对目标品种优化
TakeProfitPoints、StopLossPoints与BreakEvenTriggerPoints,因为原始数值针对外汇点值设定。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// True Scalper Profit Lock strategy converted from MetaTrader 5.
/// Combines short-term exponential moving averages with RSI filters, profit locking and abandon logic.
/// </summary>
public class TrueScalperProfitLockStrategy : Strategy
{
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<decimal> _rsiThreshold;
private readonly StrategyParam<bool> _useRsiMethodA;
private readonly StrategyParam<bool> _useRsiMethodB;
private readonly StrategyParam<bool> _useAbandonMethodA;
private readonly StrategyParam<bool> _useAbandonMethodB;
private readonly StrategyParam<int> _abandonBars;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<bool> _useProfitLock;
private readonly StrategyParam<decimal> _breakEvenTriggerPoints;
private readonly StrategyParam<decimal> _breakEvenPoints;
private readonly StrategyParam<bool> _liveTrading;
private readonly StrategyParam<bool> _isMiniAccount;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<DataType> _candleType;
private decimal? _entryPrice;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal? _previousRsi;
private decimal _currentVolume;
private bool _isLongPosition;
private bool _pendingReverseToBuy;
private bool _pendingReverseToSell;
private int _barsSinceEntry;
private DateTimeOffset? _lastCandleTime;
private bool _breakEvenApplied;
/// <summary>
/// Base order size expressed in lots.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Take profit distance in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// RSI calculation length.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// RSI decision threshold.
/// </summary>
public decimal RsiThreshold
{
get => _rsiThreshold.Value;
set => _rsiThreshold.Value = value;
}
/// <summary>
/// Enable RSI crossing logic.
/// </summary>
public bool UseRsiMethodA
{
get => _useRsiMethodA.Value;
set => _useRsiMethodA.Value = value;
}
/// <summary>
/// Enable RSI polarity logic.
/// </summary>
public bool UseRsiMethodB
{
get => _useRsiMethodB.Value;
set => _useRsiMethodB.Value = value;
}
/// <summary>
/// Force reverse direction after abandon timeout.
/// </summary>
public bool UseAbandonMethodA
{
get => _useAbandonMethodA.Value;
set => _useAbandonMethodA.Value = value;
}
/// <summary>
/// Close the trade after abandon timeout without forcing direction.
/// </summary>
public bool UseAbandonMethodB
{
get => _useAbandonMethodB.Value;
set => _useAbandonMethodB.Value = value;
}
/// <summary>
/// Number of finished candles before abandon logic triggers.
/// </summary>
public int AbandonBars
{
get => _abandonBars.Value;
set => _abandonBars.Value = value;
}
/// <summary>
/// Enable balance based position sizing.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Risk percentage used in money management.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Enable break even stop adjustment.
/// </summary>
public bool UseProfitLock
{
get => _useProfitLock.Value;
set => _useProfitLock.Value = value;
}
/// <summary>
/// Profit distance that triggers break even move.
/// </summary>
public decimal BreakEvenTriggerPoints
{
get => _breakEvenTriggerPoints.Value;
set => _breakEvenTriggerPoints.Value = value;
}
/// <summary>
/// Stop offset applied once break even activates.
/// </summary>
public decimal BreakEvenPoints
{
get => _breakEvenPoints.Value;
set => _breakEvenPoints.Value = value;
}
/// <summary>
/// Use live trading sizing adjustments.
/// </summary>
public bool LiveTrading
{
get => _liveTrading.Value;
set => _liveTrading.Value = value;
}
/// <summary>
/// Treat account as mini when applying live adjustments.
/// </summary>
public bool IsMiniAccount
{
get => _isMiniAccount.Value;
set => _isMiniAccount.Value = value;
}
/// <summary>
/// Maximum simultaneous trades allowed by the original logic.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Candle type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TrueScalperProfitLockStrategy"/> class.
/// </summary>
public TrueScalperProfitLockStrategy()
{
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Lots", "Base trade volume", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 44m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take profit distance in steps", "Risk")
.SetOptimize(20m, 80m, 5m);
_stopLossPoints = Param(nameof(StopLossPoints), 90m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop loss distance in steps", "Risk")
.SetOptimize(50m, 150m, 10m);
_fastPeriod = Param(nameof(FastPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Fast EMA length", "Signals");
_slowPeriod = Param(nameof(SlowPeriod), 7)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Slow EMA length", "Signals");
_rsiLength = Param(nameof(RsiLength), 2)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "RSI calculation length", "Signals");
_rsiThreshold = Param(nameof(RsiThreshold), 50m)
.SetDisplay("RSI Threshold", "RSI boundary for polarity", "Signals")
.SetOptimize(40m, 60m, 5m);
_useRsiMethodA = Param(nameof(UseRsiMethodA), true)
.SetDisplay("RSI Method A", "Use RSI crossing logic", "Signals");
_useRsiMethodB = Param(nameof(UseRsiMethodB), false)
.SetDisplay("RSI Method B", "Use RSI polarity logic", "Signals");
_useAbandonMethodA = Param(nameof(UseAbandonMethodA), true)
.SetDisplay("Abandon Method A", "Force reverse after timeout", "Management");
_useAbandonMethodB = Param(nameof(UseAbandonMethodB), false)
.SetDisplay("Abandon Method B", "Only close after timeout", "Management");
_abandonBars = Param(nameof(AbandonBars), 101)
.SetGreaterThanZero()
.SetDisplay("Abandon Bars", "Bars before abandon logic", "Management");
_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
.SetDisplay("Money Management", "Enable balance based sizing", "Risk");
_riskPercent = Param(nameof(RiskPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Risk percentage per trade", "Risk");
_useProfitLock = Param(nameof(UseProfitLock), true)
.SetDisplay("Use Profit Lock", "Move stop to break even", "Risk");
_breakEvenTriggerPoints = Param(nameof(BreakEvenTriggerPoints), 25m)
.SetGreaterThanZero()
.SetDisplay("BreakEven Trigger", "Profit distance before break even", "Risk");
_breakEvenPoints = Param(nameof(BreakEvenPoints), 3m)
.SetGreaterThanZero()
.SetDisplay("BreakEven Offset", "Offset applied at break even", "Risk");
_liveTrading = Param(nameof(LiveTrading), false)
.SetDisplay("Live Trading", "Apply live sizing adjustments", "Risk");
_isMiniAccount = Param(nameof(IsMiniAccount), false)
.SetDisplay("Mini Account", "Treat account as mini", "Risk");
_maxPositions = Param(nameof(MaxPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum simultaneous trades", "Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle type for processing", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_previousRsi = null;
_currentVolume = 0m;
_isLongPosition = false;
_pendingReverseToBuy = false;
_pendingReverseToSell = false;
_barsSinceEntry = 0;
_lastCandleTime = null;
_breakEvenApplied = false;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastEma = new EMA { Length = FastPeriod };
var slowEma = new EMA { Length = SlowPeriod };
var rsi = new RSI { Length = RsiLength };
SubscribeCandles(CandleType)
.Bind(fastEma, slowEma, rsi, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastEma, decimal slowEma, decimal rsi)
{
if (candle.State != CandleStates.Finished)
return;
UpdateBarCounter(candle);
var step = GetPriceStep();
ApplyAbandonLogic();
if (Position != 0)
{
ApplyProfitLock(step, candle);
if (TryExitByTargets(candle))
{
_pendingReverseToBuy = false;
_pendingReverseToSell = false;
}
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousRsi = rsi;
return;
}
var (rsiPositive, rsiNegative) = EvaluateRsiSignals(rsi);
var buySignal = fastEma > slowEma + step && rsiNegative;
var sellSignal = fastEma < slowEma - step && rsiPositive;
TryEnterPosition(candle, step, buySignal, sellSignal);
_previousRsi = rsi;
}
private void UpdateBarCounter(ICandleMessage candle)
{
if (_lastCandleTime == candle.OpenTime)
return;
if (Position != 0 && _lastCandleTime != null)
_barsSinceEntry++;
else if (Position == 0)
_barsSinceEntry = 0;
_lastCandleTime = candle.OpenTime;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0)
step = 0.0001m;
return step;
}
private void ApplyAbandonLogic()
{
if (Position == 0 || AbandonBars <= 0)
return;
if (_barsSinceEntry < AbandonBars)
return;
if (UseAbandonMethodA)
{
if (_isLongPosition && Position > 0)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
_pendingReverseToSell = true;
_pendingReverseToBuy = false;
}
else if (!_isLongPosition && Position < 0)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
_pendingReverseToBuy = true;
_pendingReverseToSell = false;
}
}
else if (UseAbandonMethodB)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
_pendingReverseToBuy = false;
_pendingReverseToSell = false;
}
}
private void ApplyProfitLock(decimal step, ICandleMessage candle)
{
if (!UseProfitLock || _entryPrice is not decimal entry || _stopLossPrice is not decimal stop)
return;
if (_isLongPosition && Position > 0)
{
if (!_breakEvenApplied && stop < entry && BreakEvenTriggerPoints > 0m && candle.HighPrice >= entry + step * BreakEvenTriggerPoints)
{
_stopLossPrice = entry + step * BreakEvenPoints;
_breakEvenApplied = true;
}
}
else if (!_isLongPosition && Position < 0)
{
if (!_breakEvenApplied && stop > entry && BreakEvenTriggerPoints > 0m && candle.LowPrice <= entry - step * BreakEvenTriggerPoints)
{
_stopLossPrice = entry - step * BreakEvenPoints;
_breakEvenApplied = true;
}
}
}
private bool TryExitByTargets(ICandleMessage candle)
{
if (_entryPrice is null || _stopLossPrice is null || _takeProfitPrice is null)
return false;
if (_isLongPosition && Position > 0)
{
if (candle.HighPrice >= _takeProfitPrice)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
return true;
}
if (candle.LowPrice <= _stopLossPrice)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
return true;
}
}
else if (!_isLongPosition && Position < 0)
{
if (candle.LowPrice <= _takeProfitPrice)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
return true;
}
if (candle.HighPrice >= _stopLossPrice)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
ResetTradeState();
return true;
}
}
return false;
}
private (bool positive, bool negative) EvaluateRsiSignals(decimal currentRsi)
{
var positive = false;
var negative = false;
if (UseRsiMethodA && _previousRsi is decimal prev)
{
if (currentRsi > RsiThreshold && prev < RsiThreshold)
{
positive = true;
negative = false;
}
else if (currentRsi < RsiThreshold && prev > RsiThreshold)
{
positive = false;
negative = true;
}
}
if (UseRsiMethodB)
{
if (currentRsi > RsiThreshold)
{
positive = true;
negative = false;
}
else if (currentRsi < RsiThreshold)
{
positive = false;
negative = true;
}
}
return (positive, negative);
}
private void TryEnterPosition(ICandleMessage candle, decimal step, bool buySignal, bool sellSignal)
{
if (MaxPositions <= 0)
return;
var volume = CalculateEntryVolume();
if (volume <= 0)
return;
if ((_pendingReverseToBuy || buySignal) && Position <= 0)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0)
return;
BuyMarket();
InitializeTradeState(candle, step, volume, true);
_pendingReverseToBuy = false;
_pendingReverseToSell = false;
}
else if ((_pendingReverseToSell || sellSignal) && Position >= 0)
{
var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0)
return;
SellMarket();
InitializeTradeState(candle, step, volume, false);
_pendingReverseToBuy = false;
_pendingReverseToSell = false;
}
}
private decimal CalculateEntryVolume()
{
var volume = InitialVolume;
if (UseMoneyManagement)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0)
{
var managed = Math.Ceiling(balance * RiskPercent / 10000m) / 10m;
if (managed < 0.1m)
managed = InitialVolume;
if (managed > 1m)
managed = Math.Ceiling(managed);
if (LiveTrading)
{
if (IsMiniAccount)
managed *= 10m;
else if (managed < 1m)
managed = 1m;
}
if (managed > 100m)
managed = 100m;
volume = managed;
}
}
return Math.Max(volume, 0m);
}
private void InitializeTradeState(ICandleMessage candle, decimal step, decimal volume, bool isLong)
{
_isLongPosition = isLong;
_entryPrice = candle.ClosePrice;
_currentVolume = volume;
_breakEvenApplied = false;
_barsSinceEntry = 0;
_lastCandleTime = candle.OpenTime;
if (isLong)
{
_stopLossPrice = _entryPrice - step * StopLossPoints;
_takeProfitPrice = _entryPrice + step * TakeProfitPoints;
}
else
{
_stopLossPrice = _entryPrice + step * StopLossPoints;
_takeProfitPrice = _entryPrice - step * TakeProfitPoints;
}
}
private void ResetTradeState()
{
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_currentVolume = 0m;
_breakEvenApplied = false;
_barsSinceEntry = 0;
}
}
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.Indicators import ExponentialMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class true_scalper_profit_lock_strategy(Strategy):
def __init__(self):
super(true_scalper_profit_lock_strategy, self).__init__()
self._tp_points = self.Param("TakeProfitPoints", 44.0)
self._sl_points = self.Param("StopLossPoints", 90.0)
self._fast_period = self.Param("FastPeriod", 3)
self._slow_period = self.Param("SlowPeriod", 7)
self._rsi_length = self.Param("RsiLength", 2)
self._rsi_threshold = self.Param("RsiThreshold", 50.0)
self._abandon_bars = self.Param("AbandonBars", 101)
self._be_trigger = self.Param("BreakEvenTrigger", 25.0)
self._be_offset = self.Param("BreakEvenOffset", 3.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(true_scalper_profit_lock_strategy, self).OnReseted()
self._entry_price = 0
self._stop_price = 0
self._take_price = 0
self._prev_rsi = 0
self._is_long = False
self._bars_since_entry = 0
self._be_applied = False
def OnStarted2(self, time):
super(true_scalper_profit_lock_strategy, self).OnStarted2(time)
self._entry_price = 0
self._stop_price = 0
self._take_price = 0
self._prev_rsi = 0
self._is_long = False
self._bars_since_entry = 0
self._be_applied = False
fast = ExponentialMovingAverage()
fast.Length = self._fast_period.Value
slow = ExponentialMovingAverage()
slow.Length = self._slow_period.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, rsi, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, fast_val, slow_val, rsi_val):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_val)
slow_val = float(slow_val)
rsi_val = float(rsi_val)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
step = 1.0
# Count bars since entry
if self.Position != 0:
self._bars_since_entry += 1
else:
self._bars_since_entry = 0
# Abandon logic
if self.Position != 0 and self._bars_since_entry >= self._abandon_bars.Value:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_trade()
self._prev_rsi = rsi_val
return
# Break-even
if self.Position != 0 and self._entry_price > 0 and not self._be_applied:
trigger = self._be_trigger.Value * step
offset = self._be_offset.Value * step
if self._is_long and self.Position > 0:
if high >= self._entry_price + trigger:
new_stop = self._entry_price + offset
if new_stop > self._stop_price:
self._stop_price = new_stop
self._be_applied = True
elif not self._is_long and self.Position < 0:
if low <= self._entry_price - trigger:
new_stop = self._entry_price - offset
if self._stop_price == 0 or new_stop < self._stop_price:
self._stop_price = new_stop
self._be_applied = True
# SL/TP exits
if self._is_long and self.Position > 0:
if self._take_price > 0 and high >= self._take_price:
self.SellMarket()
self._reset_trade()
self._prev_rsi = rsi_val
return
if self._stop_price > 0 and low <= self._stop_price:
self.SellMarket()
self._reset_trade()
self._prev_rsi = rsi_val
return
elif not self._is_long and self.Position < 0:
if self._take_price > 0 and low <= self._take_price:
self.BuyMarket()
self._reset_trade()
self._prev_rsi = rsi_val
return
if self._stop_price > 0 and high >= self._stop_price:
self.BuyMarket()
self._reset_trade()
self._prev_rsi = rsi_val
return
# RSI signals
rsi_positive = False
rsi_negative = False
if self._prev_rsi > 0:
thr = self._rsi_threshold.Value
if rsi_val > thr and self._prev_rsi < thr:
rsi_positive = True
elif rsi_val < thr and self._prev_rsi > thr:
rsi_negative = True
buy_signal = fast_val > slow_val + step and rsi_negative
sell_signal = fast_val < slow_val - step and rsi_positive
if buy_signal and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._is_long = True
self._stop_price = close - step * self._sl_points.Value
self._take_price = close + step * self._tp_points.Value
self._be_applied = False
self._bars_since_entry = 0
elif sell_signal and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._is_long = False
self._stop_price = close + step * self._sl_points.Value
self._take_price = close - step * self._tp_points.Value
self._be_applied = False
self._bars_since_entry = 0
self._prev_rsi = rsi_val
def _reset_trade(self):
self._entry_price = 0
self._stop_price = 0
self._take_price = 0
self._be_applied = False
self._bars_since_entry = 0
def CreateClone(self):
return true_scalper_profit_lock_strategy()