Zone Recovery Area 策略
概述
Zone Recovery Area Strategy 是将 MetaTrader 专家顾问 “Zone Recovery Area”(MQL/20266)完整移植到 StockSharp 高级 API 的版本。策略保留了原有的区间恢复(zone recovery)对冲逻辑,并将主要参数全部公开,方便在不修改代码的情况下调优。开仓后,系统会围绕基准价格交替建立多、空头寸,当价格离开或重新进入预设区域时加仓,以期逐步弥补浮动亏损并最终以盈利关闭整个组合。
主要特性:
- 使用快、慢两条简单移动平均线(SMA)结合月度 MACD(12/26/9)作为趋势过滤器。
- 实现 zone recovery 对冲机制:第一笔交易确定基准价,后续对冲单在价格穿越区域边界或回到基准价时触发。
- 支持三种盈利退出方式:绝对金额、账户百分比、追踪止盈。
- 每一步加仓体量可按倍数递增(类马丁策略)或按固定增量增加。
数据与指标
- 主级别 K 线: 用户自定义的入场与管理周期,默认 30 分钟。
- 月度 K 线: 若无原生月线,可由更小周期合成,用于计算 MACD。
- 指标:
- 主周期上的两条 SMA。
- 月度 MACD(含信号线)。
交易流程
- 趋势确认
- 等待两条 SMA 与月度 MACD 均形成有效数值。
- 多头条件: 前一根 K 线中快 SMA 低于慢 SMA,且 MACD 主线高于信号线。
- 空头条件: 前一根 K 线中快 SMA 高于慢 SMA,且 MACD 主线低于信号线。
- 启动恢复周期
- 出现多头(空头)信号时,以
InitialVolume开多(空)头寸,并记录成交价为基准价。 - 清空内部计数器与盈利跟踪变量,开始新的恢复周期。
- 出现多头(空头)信号时,以
- 恢复引擎
- 计算两个关键价位:恢复区间(
ZoneRecoveryPips)与盈利目标(TakeProfitPips)。 - 周期运行过程中,每根完结的 K 线都需要检查:
- 价格到达盈利目标时立即平掉所有净头寸,结束周期;
- 若达到金额或百分比目标,或追踪止盈触发,也会立即平仓;
- 否则判断是否需要新对冲单:
- 多头周期:跌破
base - zone时加空单,重新站回基准价时加多单; - 空头周期:突破
base + zone时加多单,回落至基准价以下时加空单。
- 多头周期:跌破
- 多、空方向自动交替;每笔对冲单的手数根据设置自动放大或累加。
MaxTrades控制单个周期内的最大交易次数。
- 计算两个关键价位:恢复区间(
- 盈利管理
UseMoneyTakeProfit:未实现利润达到设定金额时结束周期。UsePercentTakeProfit:未实现利润达到账户价值的一定百分比时结束周期。EnableTrailing:利润超过TrailingStartProfit后记录峰值,若回撤超过TrailingDrawdown即平仓。
策略使用 StockSharp 的高阶下单函数 BuyMarket/SellMarket,无需直接处理底层订单对象。
参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
30 分钟 | 入场与管理所用的主周期。 |
MonthlyCandleType |
30 天 | 计算月度 MACD 的周期。 |
FastMaLength |
20 | 快速 SMA 的周期。 |
SlowMaLength |
200 | 慢速 SMA 的周期。 |
TakeProfitPips |
150 | 从基准价起算的总体盈利目标。 |
ZoneRecoveryPips |
50 | 恢复区间半宽度。 |
InitialVolume |
1 | 周期第一笔订单的手数。 |
UseVolumeMultiplier |
true | 是否按倍数放大后续订单。 |
VolumeMultiplier |
2 | 使用倍增模式时的乘数。 |
VolumeIncrement |
0.5 | 使用加法模式时的增量。 |
MaxTrades |
6 | 单个恢复周期内的最大订单数。 |
UseMoneyTakeProfit |
false | 启用金额止盈。 |
MoneyTakeProfit |
40 | 金额止盈目标。 |
UsePercentTakeProfit |
false | 启用百分比止盈。 |
PercentTakeProfit |
5 | 百分比止盈目标。 |
EnableTrailing |
true | 启用追踪止盈。 |
TrailingStartProfit |
40 | 追踪止盈启动阈值。 |
TrailingDrawdown |
10 | 允许的利润回吐。 |
点值转换:
TakeProfitPips与ZoneRecoveryPips会根据标的的PriceStep转换为价格偏移,请确保证券提供正确的最小价位与步长价值。
使用建议
- 在 Designer/API/Runner 中加载策略,并指定交易品种与投资组合。
- 根据标的波动率与风险承受能力调整各项参数。
- 确保历史数据长度足够,便于 SMA 与 MACD 完成预热。
- 密切关注保证金占用,倍增模式下仓位会迅速扩大。
- 先在回测或模拟环境验证,再考虑实盘运行。
风险提示
- 区间恢复/马丁策略在强趋势行情中可能累积巨额头寸,必须使用
MaxTrades和合理的参数限制风险。 - StockSharp 采用净持仓模型,策略会根据
PriceStep/StepPrice计算组合盈亏,建议与券商数据进行核对。 - 金额与百分比止盈依赖投资组合估值,回测时请确认
BeginValue、CurrentValue等字段有效。 - 策略未设置硬止损,如有需要应在账户层面增加其他风控措施。
文件说明
CS/ZoneRecoveryAreaStrategy.cs— 策略实现。README.md— 英文说明。README_ru.md— 俄文说明。README_zh.md— 中文说明(本文件)。
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>
/// Zone recovery hedging strategy converted from MetaTrader expert advisor.
/// The strategy alternates buy and sell positions around a base price to recover drawdowns.
/// </summary>
public class ZoneRecoveryAreaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _monthlyCandleType;
private readonly StrategyParam<int> _fastMaLength;
private readonly StrategyParam<int> _slowMaLength;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _zoneRecoveryPips;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<bool> _useVolumeMultiplier;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<decimal> _volumeIncrement;
private readonly StrategyParam<int> _maxTrades;
private readonly StrategyParam<bool> _useMoneyTakeProfit;
private readonly StrategyParam<decimal> _moneyTakeProfit;
private readonly StrategyParam<bool> _usePercentTakeProfit;
private readonly StrategyParam<decimal> _percentTakeProfit;
private readonly StrategyParam<bool> _enableTrailing;
private readonly StrategyParam<decimal> _trailingStartProfit;
private readonly StrategyParam<decimal> _trailingDrawdown;
private SimpleMovingAverage _fastMa = null!;
private SimpleMovingAverage _slowMa = null!;
private MovingAverageConvergenceDivergenceSignal _monthlyMacd = null!;
private decimal _prevFast;
private decimal _prevSlow;
private bool _maInitialized;
private bool _macdReady;
private decimal _macdMain;
private decimal _macdSignal;
private bool _isLongCycle;
private decimal _cycleBasePrice;
private int _nextStepIndex;
private decimal _peakCycleProfit;
private readonly List<TradeStep> _steps = new();
/// <summary>
/// Initializes a new instance of <see cref="ZoneRecoveryAreaStrategy"/>.
/// </summary>
public ZoneRecoveryAreaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Entry Candle", "Timeframe used for entries", "General");
_monthlyCandleType = Param(nameof(MonthlyCandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Monthly Candle", "Timeframe used for MACD filter", "General");
_fastMaLength = Param(nameof(FastMaLength), 20)
.SetGreaterThanZero()
.SetDisplay("Fast MA", "Fast moving average period", "Trend Filter");
_slowMaLength = Param(nameof(SlowMaLength), 200)
.SetGreaterThanZero()
.SetDisplay("Slow MA", "Slow moving average period", "Trend Filter");
_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Distance to close the cycle in profit", "Risk Management");
_zoneRecoveryPips = Param(nameof(ZoneRecoveryPips), 50m)
.SetGreaterThanZero()
.SetDisplay("Zone Width (pips)", "Distance that triggers hedging trades", "Risk Management");
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Volume of the first trade", "Position Sizing");
_useVolumeMultiplier = Param(nameof(UseVolumeMultiplier), true)
.SetDisplay("Use Multiplier", "If true the next trades multiply the previous volume", "Position Sizing");
_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Factor applied when increasing volume", "Position Sizing");
_volumeIncrement = Param(nameof(VolumeIncrement), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Volume Increment", "Additional volume when multiplier is disabled", "Position Sizing");
_maxTrades = Param(nameof(MaxTrades), 6)
.SetGreaterThanZero()
.SetDisplay("Max Trades", "Maximum number of trades in one cycle", "Risk Management");
_useMoneyTakeProfit = Param(nameof(UseMoneyTakeProfit), false)
.SetDisplay("Money Take Profit", "Enable profit target in account currency", "Risk Management");
_moneyTakeProfit = Param(nameof(MoneyTakeProfit), 40m)
.SetGreaterThanZero()
.SetDisplay("Take Profit $", "Target profit in account currency", "Risk Management");
_usePercentTakeProfit = Param(nameof(UsePercentTakeProfit), false)
.SetDisplay("Percent Take Profit", "Enable profit target based on account balance", "Risk Management");
_percentTakeProfit = Param(nameof(PercentTakeProfit), 5m)
.SetGreaterThanZero()
.SetDisplay("Take Profit %", "Target profit as a percentage of balance", "Risk Management");
_enableTrailing = Param(nameof(EnableTrailing), true)
.SetDisplay("Trailing", "Enable trailing profit lock", "Risk Management");
_trailingStartProfit = Param(nameof(TrailingStartProfit), 40m)
.SetGreaterThanZero()
.SetDisplay("Trailing Start", "Profit required before trailing starts", "Risk Management");
_trailingDrawdown = Param(nameof(TrailingDrawdown), 10m)
.SetGreaterThanZero()
.SetDisplay("Trailing Step", "Maximum profit giveback before exit", "Risk Management");
}
/// <summary>
/// Working candle type for entries.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Monthly candle type used for the MACD filter.
/// </summary>
public DataType MonthlyCandleType
{
get => _monthlyCandleType.Value;
set => _monthlyCandleType.Value = value;
}
/// <summary>
/// Fast moving average period.
/// </summary>
public int FastMaLength
{
get => _fastMaLength.Value;
set => _fastMaLength.Value = value;
}
/// <summary>
/// Slow moving average period.
/// </summary>
public int SlowMaLength
{
get => _slowMaLength.Value;
set => _slowMaLength.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Zone width in pips for opening hedging trades.
/// </summary>
public decimal ZoneRecoveryPips
{
get => _zoneRecoveryPips.Value;
set => _zoneRecoveryPips.Value = value;
}
/// <summary>
/// Volume of the first trade in a cycle.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Use multiplicative volume scaling.
/// </summary>
public bool UseVolumeMultiplier
{
get => _useVolumeMultiplier.Value;
set => _useVolumeMultiplier.Value = value;
}
/// <summary>
/// Multiplier applied to the previous volume.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Additional volume added when multiplier is disabled.
/// </summary>
public decimal VolumeIncrement
{
get => _volumeIncrement.Value;
set => _volumeIncrement.Value = value;
}
/// <summary>
/// Maximum number of trades per recovery cycle.
/// </summary>
public int MaxTrades
{
get => _maxTrades.Value;
set => _maxTrades.Value = value;
}
/// <summary>
/// Enable profit target in account currency.
/// </summary>
public bool UseMoneyTakeProfit
{
get => _useMoneyTakeProfit.Value;
set => _useMoneyTakeProfit.Value = value;
}
/// <summary>
/// Profit target in account currency.
/// </summary>
public decimal MoneyTakeProfit
{
get => _moneyTakeProfit.Value;
set => _moneyTakeProfit.Value = value;
}
/// <summary>
/// Enable profit target based on account percentage.
/// </summary>
public bool UsePercentTakeProfit
{
get => _usePercentTakeProfit.Value;
set => _usePercentTakeProfit.Value = value;
}
/// <summary>
/// Profit target as a percentage of account balance.
/// </summary>
public decimal PercentTakeProfit
{
get => _percentTakeProfit.Value;
set => _percentTakeProfit.Value = value;
}
/// <summary>
/// Enable trailing profit lock.
/// </summary>
public bool EnableTrailing
{
get => _enableTrailing.Value;
set => _enableTrailing.Value = value;
}
/// <summary>
/// Profit level where trailing begins.
/// </summary>
public decimal TrailingStartProfit
{
get => _trailingStartProfit.Value;
set => _trailingStartProfit.Value = value;
}
/// <summary>
/// Allowed drawdown from the peak profit before closing.
/// </summary>
public decimal TrailingDrawdown
{
get => _trailingDrawdown.Value;
set => _trailingDrawdown.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
{
yield return (Security, CandleType);
yield return (Security, MonthlyCandleType);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_steps.Clear();
_prevFast = 0m;
_prevSlow = 0m;
_maInitialized = false;
_macdReady = false;
_macdMain = 0m;
_macdSignal = 0m;
_isLongCycle = false;
_cycleBasePrice = 0m;
_nextStepIndex = 0;
_peakCycleProfit = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = new SimpleMovingAverage { Length = FastMaLength };
_slowMa = new SimpleMovingAverage { Length = SlowMaLength };
_monthlyMacd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = 12 },
LongMa = { Length = 26 }
},
SignalMa = { Length = 9 }
};
var mainSubscription = SubscribeCandles(CandleType);
mainSubscription
.Bind(_fastMa, _slowMa, ProcessMainCandle)
.Start();
var monthlySubscription = SubscribeCandles(MonthlyCandleType);
monthlySubscription
.Bind(ProcessMonthlyCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, mainSubscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawOwnTrades(area);
// MACD is manually processed so cannot be drawn via DrawIndicator
}
}
private void ProcessMonthlyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var macdResult = _monthlyMacd.Process(candle);
if (macdResult.IsEmpty || !_monthlyMacd.IsFormed)
return;
var macd = (MovingAverageConvergenceDivergenceSignalValue)macdResult;
if (macd.Macd is not decimal macdLine || macd.Signal is not decimal signalLine)
return;
_macdMain = macdLine;
_macdSignal = signalLine;
_macdReady = true;
}
private void ProcessMainCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_macdReady)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (!_maInitialized)
{
_prevFast = fastValue;
_prevSlow = slowValue;
_maInitialized = true;
return;
}
if (_steps.Count > 0)
{
HandleExistingCycle(candle.ClosePrice);
}
else
{
TryStartCycle(candle.ClosePrice);
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
private void TryStartCycle(decimal price)
{
var macdBullish = _macdMain > _macdSignal;
var macdBearish = _macdMain < _macdSignal;
var bullishSetup = _prevFast < _prevSlow && macdBullish;
var bearishSetup = _prevFast > _prevSlow && macdBearish;
if (bullishSetup)
{
StartCycle(true, price);
}
else if (bearishSetup)
{
StartCycle(false, price);
}
}
private void StartCycle(bool isLong, decimal price)
{
if (InitialVolume <= 0m)
return;
_steps.Clear();
_isLongCycle = isLong;
_cycleBasePrice = price;
_nextStepIndex = 1;
_peakCycleProfit = 0m;
ExecuteOrder(isLong, InitialVolume, price);
}
private void HandleExistingCycle(decimal price)
{
var takeProfitOffset = GetPriceOffset(TakeProfitPips);
if (takeProfitOffset > 0m)
{
if (_isLongCycle && price >= _cycleBasePrice + takeProfitOffset)
{
CloseCycle();
return;
}
if (!_isLongCycle && price <= _cycleBasePrice - takeProfitOffset)
{
CloseCycle();
return;
}
}
var cycleProfit = CalculateCycleProfit(price);
if (UseMoneyTakeProfit && MoneyTakeProfit > 0m && cycleProfit >= MoneyTakeProfit)
{
CloseCycle();
return;
}
if (UsePercentTakeProfit && PercentTakeProfit > 0m && TryGetPercentTarget(out var percentTarget) && cycleProfit >= percentTarget)
{
CloseCycle();
return;
}
if (EnableTrailing && TrailingStartProfit > 0m && TrailingDrawdown > 0m)
{
if (cycleProfit >= TrailingStartProfit)
{
_peakCycleProfit = Math.Max(_peakCycleProfit, cycleProfit);
}
if (_peakCycleProfit > 0m && cycleProfit <= _peakCycleProfit - TrailingDrawdown)
{
CloseCycle();
return;
}
}
else
{
_peakCycleProfit = 0m;
}
if (_steps.Count >= MaxTrades)
return;
if (!ShouldOpenNextTrade(price))
return;
var nextIsBuy = GetNextDirection();
var volume = GetNextVolume();
ExecuteOrder(nextIsBuy, volume, price);
_nextStepIndex++;
}
private bool ShouldOpenNextTrade(decimal price)
{
var zoneOffset = GetPriceOffset(ZoneRecoveryPips);
if (zoneOffset <= 0m)
return false;
var nextIsBuy = GetNextDirection();
if (_isLongCycle)
{
if (nextIsBuy)
return price >= _cycleBasePrice;
return price <= _cycleBasePrice - zoneOffset;
}
if (nextIsBuy)
return price >= _cycleBasePrice + zoneOffset;
return price <= _cycleBasePrice;
}
private bool GetNextDirection()
{
var isOddStep = _nextStepIndex % 2 == 1;
if (_isLongCycle)
return !isOddStep;
return isOddStep;
}
private decimal GetNextVolume()
{
if (_steps.Count == 0)
return InitialVolume;
var lastVolume = _steps[^1].Volume;
decimal nextVolume;
if (UseVolumeMultiplier)
{
nextVolume = lastVolume * VolumeMultiplier;
}
else
{
nextVolume = lastVolume + VolumeIncrement;
}
return nextVolume <= 0m ? InitialVolume : decimal.Round(nextVolume, 6);
}
private decimal CalculateCycleProfit(decimal price)
{
if (_steps.Count == 0 || Security == null)
return 0m;
var priceStep = Security.PriceStep ?? 0m;
var stepPrice = Security.PriceStep ?? 0m;
if (priceStep <= 0m || stepPrice <= 0m)
return 0m;
decimal pnl = 0m;
foreach (var step in _steps)
{
var diff = price - step.Price;
var stepsCount = diff / priceStep;
var direction = step.IsBuy ? 1m : -1m;
pnl += stepsCount * stepPrice * step.Volume * direction;
}
return pnl;
}
private bool TryGetPercentTarget(out decimal target)
{
target = 0m;
if (Portfolio == null)
return false;
var balance = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
if (balance <= 0m)
return false;
target = balance * PercentTakeProfit / 100m;
return true;
}
private decimal GetPriceOffset(decimal pips)
{
if (Security == null)
return 0m;
var priceStep = Security.PriceStep ?? 0m;
return priceStep <= 0m ? 0m : pips * priceStep;
}
private void ExecuteOrder(bool isBuy, decimal volume, decimal price)
{
if (volume <= 0m)
return;
if (isBuy)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
_steps.Add(new TradeStep(isBuy, price, volume));
}
private void CloseCycle()
{
if (Position > 0m)
{
SellMarket(Position);
}
else if (Position < 0m)
{
BuyMarket(Math.Abs(Position));
}
_steps.Clear();
_nextStepIndex = 0;
_cycleBasePrice = 0m;
_peakCycleProfit = 0m;
}
private sealed record TradeStep(bool IsBuy, decimal Price, decimal Volume);
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType
from System import TimeSpan, Math
class zone_recovery_area_strategy(Strategy):
def __init__(self):
super(zone_recovery_area_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._fast_ma_length = self.Param("FastMaLength", 20)
self._slow_ma_length = self.Param("SlowMaLength", 200)
self._take_profit_pips = self.Param("TakeProfitPips", 150.0)
self._zone_recovery_pips = self.Param("ZoneRecoveryPips", 50.0)
self._initial_volume = self.Param("InitialVolume", 1.0)
self._use_volume_multiplier = self.Param("UseVolumeMultiplier", True)
self._volume_multiplier = self.Param("VolumeMultiplier", 2.0)
self._volume_increment = self.Param("VolumeIncrement", 0.5)
self._max_trades = self.Param("MaxTrades", 6)
self._enable_trailing = self.Param("EnableTrailing", True)
self._trailing_start_profit = self.Param("TrailingStartProfit", 40.0)
self._trailing_drawdown = self.Param("TrailingDrawdown", 10.0)
self._fast_ma = None
self._slow_ma = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._ma_initialized = False
self._is_long_cycle = False
self._cycle_base_price = 0.0
self._next_step_index = 0
self._peak_cycle_profit = 0.0
self._steps = []
@property
def CandleType(self):
return self._candle_type.Value
@property
def InitialVolume(self):
return self._initial_volume.Value
def OnStarted2(self, time):
super(zone_recovery_area_strategy, self).OnStarted2(time)
self._fast_ma = SimpleMovingAverage()
self._fast_ma.Length = self._fast_ma_length.Value
self._slow_ma = SimpleMovingAverage()
self._slow_ma.Length = self._slow_ma_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._fast_ma, self._slow_ma, self._process_candle).Start()
def _process_candle(self, candle, fast_val, slow_val):
fast_value = float(fast_val)
slow_value = float(slow_val)
if not self._fast_ma.IsFormed or not self._slow_ma.IsFormed:
self._prev_fast = fast_value
self._prev_slow = slow_value
return
if not self._ma_initialized:
self._prev_fast = fast_value
self._prev_slow = slow_value
self._ma_initialized = True
return
price = float(candle.ClosePrice)
if len(self._steps) > 0:
self._handle_existing_cycle(price)
else:
self._try_start_cycle(price)
self._prev_fast = fast_value
self._prev_slow = slow_value
def _try_start_cycle(self, price):
bullish = self._prev_fast < self._prev_slow
bearish = self._prev_fast > self._prev_slow
if bullish:
self._start_cycle(True, price)
elif bearish:
self._start_cycle(False, price)
def _start_cycle(self, is_long, price):
vol = self.InitialVolume
if vol <= 0:
return
self._steps = []
self._is_long_cycle = is_long
self._cycle_base_price = price
self._next_step_index = 1
self._peak_cycle_profit = 0.0
self._execute_order(is_long, vol, price)
def _handle_existing_cycle(self, price):
tp_offset = self._get_price_offset(self._take_profit_pips.Value)
if tp_offset > 0:
if self._is_long_cycle and price >= self._cycle_base_price + tp_offset:
self._close_cycle()
return
if not self._is_long_cycle and price <= self._cycle_base_price - tp_offset:
self._close_cycle()
return
cycle_profit = self._calculate_cycle_profit(price)
if self._enable_trailing.Value and self._trailing_start_profit.Value > 0 and self._trailing_drawdown.Value > 0:
if cycle_profit >= self._trailing_start_profit.Value:
self._peak_cycle_profit = max(self._peak_cycle_profit, cycle_profit)
if self._peak_cycle_profit > 0 and cycle_profit <= self._peak_cycle_profit - self._trailing_drawdown.Value:
self._close_cycle()
return
if len(self._steps) >= self._max_trades.Value:
return
if not self._should_open_next(price):
return
next_buy = self._get_next_direction()
volume = self._get_next_volume()
self._execute_order(next_buy, volume, price)
self._next_step_index += 1
def _should_open_next(self, price):
zone_offset = self._get_price_offset(self._zone_recovery_pips.Value)
if zone_offset <= 0:
return False
next_buy = self._get_next_direction()
if self._is_long_cycle:
return price >= self._cycle_base_price if next_buy else price <= self._cycle_base_price - zone_offset
else:
return price >= self._cycle_base_price + zone_offset if next_buy else price <= self._cycle_base_price
def _get_next_direction(self):
is_odd = self._next_step_index % 2 == 1
return not is_odd if self._is_long_cycle else is_odd
def _get_next_volume(self):
if len(self._steps) == 0:
return self.InitialVolume
last_vol = self._steps[-1][2]
if self._use_volume_multiplier.Value:
nv = last_vol * self._volume_multiplier.Value
else:
nv = last_vol + self._volume_increment.Value
return nv if nv > 0 else self.InitialVolume
def _calculate_cycle_profit(self, price):
if len(self._steps) == 0 or self.Security is None:
return 0.0
ps = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 0.0
if ps <= 0:
return 0.0
pnl = 0.0
for is_buy, step_price, vol in self._steps:
diff = price - step_price
steps_count = diff / ps
direction = 1.0 if is_buy else -1.0
pnl += steps_count * ps * vol * direction
return pnl
def _get_price_offset(self, pips):
if self.Security is None:
return 0.0
ps = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 0.0
return pips * ps if ps > 0 else 0.0
def _execute_order(self, is_buy, volume, price):
if volume <= 0:
return
if is_buy:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
self._steps.append((is_buy, price, volume))
def _close_cycle(self):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
self._steps = []
self._next_step_index = 0
self._cycle_base_price = 0.0
self._peak_cycle_profit = 0.0
def OnReseted(self):
super(zone_recovery_area_strategy, self).OnReseted()
self._steps = []
self._prev_fast = 0.0
self._prev_slow = 0.0
self._ma_initialized = False
self._is_long_cycle = False
self._cycle_base_price = 0.0
self._next_step_index = 0
self._peak_cycle_profit = 0.0
def CreateClone(self):
return zone_recovery_area_strategy()