Brandy 策略 (C#)
概述
Brandy 策略移植自 MetaTrader 5 的 Brandy (barabashkakvn's edition) 智能交易系统。策略利用两条可配置的移动平均线,通过比较上一根收盘蜡烛的均线位置来判断趋势方向,同时提供以点(pip)为单位的止损、止盈以及移动止损控制。本移植版本基于 StockSharp 的高级策略 API 实现,重现了原始 EA 的交易、风控和参数结构。
策略在开盘价序列上计算一条“快线”均线,在收盘价序列上计算一条“慢线”均线,两条均线的周期、平滑方式、价格源、信号引用柱以及位移参数均可独立设置。若上一根已完成蜡烛的快线与慢线同时位于各自信号值的同一侧,则产生做多或做空信号。策略还会在每根蜡烛收盘时利用开盘价均线进行趋势校验,一旦条件被破坏便立即平仓,从而保持原始 EA 的保护逻辑。所有止损、止盈及移动止损距离均使用 pip 表示,并依据品种的最小报价步长及五位报价的特殊调整转换为绝对价格。
交易流程
- 每当生成新的完整蜡烛时,策略使用设定的平滑方法与价格源更新开盘均线与收盘均线,同时保存指定数量的历史值,以模拟 MT5
iMA函数的信号柱和位移行为。 - 当当前没有持仓时:
- 若上一柱开盘均线值大于配置的信号值(包含位移),且
- 上一柱收盘均线值也大于其信号参考值(注意:原 EA 在此处与开盘均线比较,本移植保持该特性以保证兼容), 则按固定手数开多。
- 若上述两条均线同时低于各自信号值,则开空。
- 持仓期间,每根蜡烛收盘后按以下顺序检查离场条件:
- 趋势反转:若开盘均线跌破信号值(多头)或升破信号值(空头),立即以市价平仓;
- 移动止损:当浮动利润超过“移动止损距离 + 触发步长”时,将保护价位上调/下调至距离当前收盘价
TrailingStopPips的位置; - 止盈:若本根蜡烛的高/低价触及目标价,则以市价平仓;
- 止损:若蜡烛范围突破保护价位,同样平仓。
- 交易数量固定,由
TradeVolume参数决定,默认值与 MT5 版本的 0.1 手保持一致。
参数说明
| 参数 | 含义 |
|---|---|
TradeVolume |
下单手数(lots)。 |
StopLossPips |
止损距离(pip,0 表示关闭)。 |
TakeProfitPips |
止盈距离(pip,0 表示关闭)。 |
TrailingStopPips |
移动止损基础距离(pip)。需与 TrailingStepPips 配合使用。 |
TrailingStepPips |
更新移动止损前必须额外前进的 pip 数,开启移动止损时必须为正数。 |
MaClosePeriod / MaOpenPeriod |
收盘/开盘均线的周期长度。 |
MaCloseShift / MaOpenShift |
对应均线的位移量(以柱为单位)。 |
MaCloseSignalBar / MaOpenSignalBar |
用于比较的历史柱索引。0 表示最新值,1 表示上一柱,以此类推。 |
MaCloseMethod / MaOpenMethod |
均线平滑方式:SMA、EMA、SMMA、LWMA。 |
MaCloseAppliedPrice / MaOpenAppliedPrice |
均线计算所使用的蜡烛价格源:收盘、开盘、最高、最低、中位、典型、加权。 |
CandleType |
订阅的蜡烛周期。 |
实现细节
- Pip 大小基于
Security.PriceStep计算。当品种小数位数为 3 或 5 时,将步长乘以 10,以复刻 MT5 点与 pip 的换算。 - 为复现
iMA的信号柱与位移功能,代码使用受限队列缓存均线历史值,避免调用被禁止的GetValue()等指示器接口。 - 收盘均线的比较对象依旧使用开盘均线缓存,这是源代码中
iMAGet(handle_iMAOpen, MaClose_SignalBar)的原始行为,移植版本保持该特性以兼容已有参数配置。 - 止损、止盈及移动止损在蜡烛收盘时执行,逻辑尽可能贴近 EA 在服务器端修改订单的过程,同时遵守 StockSharp 高级 API 的限制。
使用建议
- 请根据原始策略的周期设定
CandleType,确保均线计算与回测时间框一致。 - 若不需要移动止损,可将
TrailingStopPips设为 0;如需启用,必须提供正数的TrailingStepPips,否则策略会在启动时抛出异常。 - 在 StockSharp 中测试时,请确认品种的
PriceStep与Decimals与目标市场匹配,以确保 pip 转换后的价格距离准确。
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>
/// Conversion of the Brandy Expert Advisor from MetaTrader 5.
/// Combines two configurable moving averages to generate entries and manages positions with trailing exits.
/// </summary>
public class BrandyStrategy : Strategy
{
/// <summary>
/// Supported moving average smoothing methods.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Sma,
/// <summary>
/// Exponential moving average.
/// </summary>
Ema,
/// <summary>
/// Smoothed moving average.
/// </summary>
Smma,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Lwma
}
/// <summary>
/// Price sources that can be fed into the moving averages.
/// </summary>
public enum AppliedPriceTypes
{
/// <summary>
/// Candle close price.
/// </summary>
Close,
/// <summary>
/// Candle open price.
/// </summary>
Open,
/// <summary>
/// Candle high price.
/// </summary>
High,
/// <summary>
/// Candle low price.
/// </summary>
Low,
/// <summary>
/// Median price of the candle (high + low) / 2.
/// </summary>
Median,
/// <summary>
/// Typical price (high + low + close) / 3.
/// </summary>
Typical,
/// <summary>
/// Weighted price (high + low + 2 * close) / 4.
/// </summary>
Weighted
}
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<int> _maClosePeriod;
private readonly StrategyParam<int> _maCloseShift;
private readonly StrategyParam<MovingAverageMethods> _maCloseMethod;
private readonly StrategyParam<AppliedPriceTypes> _maCloseAppliedPrice;
private readonly StrategyParam<int> _maCloseSignalBar;
private readonly StrategyParam<int> _maOpenPeriod;
private readonly StrategyParam<int> _maOpenShift;
private readonly StrategyParam<MovingAverageMethods> _maOpenMethod;
private readonly StrategyParam<AppliedPriceTypes> _maOpenAppliedPrice;
private readonly StrategyParam<int> _maOpenSignalBar;
private readonly StrategyParam<DataType> _candleType;
private DecimalLengthIndicator _maOpenIndicator;
private DecimalLengthIndicator _maCloseIndicator;
private decimal _pipSize;
private readonly List<decimal> _maOpenValues = [];
private readonly List<decimal> _maCloseValues = [];
private int _maxOpenQueueSize;
private int _maxCloseQueueSize;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Trading volume per order.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Step that defines how far the price must move before the trailing stop is advanced.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Period of the moving average calculated on the close series.
/// </summary>
public int MaClosePeriod
{
get => _maClosePeriod.Value;
set => _maClosePeriod.Value = value;
}
/// <summary>
/// Displacement applied to the moving average calculated on closes.
/// </summary>
public int MaCloseShift
{
get => _maCloseShift.Value;
set => _maCloseShift.Value = value;
}
/// <summary>
/// Moving average smoothing method for the close series.
/// </summary>
public MovingAverageMethods MaCloseMethod
{
get => _maCloseMethod.Value;
set => _maCloseMethod.Value = value;
}
/// <summary>
/// Price source used by the close moving average.
/// </summary>
public AppliedPriceTypes MaCloseAppliedPrice
{
get => _maCloseAppliedPrice.Value;
set => _maCloseAppliedPrice.Value = value;
}
/// <summary>
/// Bar index used as a signal reference for the close moving average.
/// </summary>
public int MaCloseSignalBar
{
get => _maCloseSignalBar.Value;
set => _maCloseSignalBar.Value = value;
}
/// <summary>
/// Period of the moving average calculated on the open series.
/// </summary>
public int MaOpenPeriod
{
get => _maOpenPeriod.Value;
set => _maOpenPeriod.Value = value;
}
/// <summary>
/// Displacement applied to the moving average calculated on opens.
/// </summary>
public int MaOpenShift
{
get => _maOpenShift.Value;
set => _maOpenShift.Value = value;
}
/// <summary>
/// Moving average smoothing method for the open series.
/// </summary>
public MovingAverageMethods MaOpenMethod
{
get => _maOpenMethod.Value;
set => _maOpenMethod.Value = value;
}
/// <summary>
/// Price source used by the open moving average.
/// </summary>
public AppliedPriceTypes MaOpenAppliedPrice
{
get => _maOpenAppliedPrice.Value;
set => _maOpenAppliedPrice.Value = value;
}
/// <summary>
/// Bar index used as a signal reference for the open moving average.
/// </summary>
public int MaOpenSignalBar
{
get => _maOpenSignalBar.Value;
set => _maOpenSignalBar.Value = value;
}
/// <summary>
/// Candle type used to feed the indicators.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="BrandyStrategy"/> class.
/// </summary>
public BrandyStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order size in lots", "General");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Distance for trailing stop", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk");
_maClosePeriod = Param(nameof(MaClosePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Close Period", "Length of MA calculated on close", "Indicators");
_maCloseShift = Param(nameof(MaCloseShift), 0)
.SetNotNegative()
.SetDisplay("MA Close Shift", "Forward shift applied to close MA", "Indicators");
_maCloseMethod = Param(nameof(MaCloseMethod), MovingAverageMethods.Ema)
.SetDisplay("MA Close Method", "Smoothing method for close MA", "Indicators");
_maCloseAppliedPrice = Param(nameof(MaCloseAppliedPrice), AppliedPriceTypes.Close)
.SetDisplay("MA Close Price", "Price source for close MA", "Indicators");
_maCloseSignalBar = Param(nameof(MaCloseSignalBar), 0)
.SetNotNegative()
.SetDisplay("MA Close Signal Bar", "Reference bar index for close MA", "Indicators");
_maOpenPeriod = Param(nameof(MaOpenPeriod), 70)
.SetGreaterThanZero()
.SetDisplay("MA Open Period", "Length of MA calculated on open", "Indicators");
_maOpenShift = Param(nameof(MaOpenShift), 0)
.SetNotNegative()
.SetDisplay("MA Open Shift", "Forward shift applied to open MA", "Indicators");
_maOpenMethod = Param(nameof(MaOpenMethod), MovingAverageMethods.Ema)
.SetDisplay("MA Open Method", "Smoothing method for open MA", "Indicators");
_maOpenAppliedPrice = Param(nameof(MaOpenAppliedPrice), AppliedPriceTypes.Close)
.SetDisplay("MA Open Price", "Price source for open MA", "Indicators");
_maOpenSignalBar = Param(nameof(MaOpenSignalBar), 0)
.SetNotNegative()
.SetDisplay("MA Open Signal Bar", "Reference bar index for open MA", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame of input candles", "General");
Volume = _tradeVolume.Value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_maOpenIndicator = null;
_maCloseIndicator = null;
_maOpenValues.Clear();
_maCloseValues.Clear();
_pipSize = 0m;
_maxOpenQueueSize = 0;
_maxCloseQueueSize = 0;
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
_maOpenIndicator = CreateMovingAverage(MaOpenMethod, MaOpenPeriod);
_maCloseIndicator = CreateMovingAverage(MaCloseMethod, MaClosePeriod);
UpdatePipSize();
UpdateQueueSizes();
Volume = _tradeVolume.Value;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_maOpenIndicator == null || _maCloseIndicator == null)
return;
var openSource = GetAppliedPrice(candle, MaOpenAppliedPrice);
var closeSource = GetAppliedPrice(candle, MaCloseAppliedPrice);
var maOpenResult = _maOpenIndicator!.Process(new DecimalIndicatorValue(_maOpenIndicator, openSource, candle.OpenTime) { IsFinal = true });
var maCloseResult = _maCloseIndicator!.Process(new DecimalIndicatorValue(_maCloseIndicator, closeSource, candle.OpenTime) { IsFinal = true });
if (maOpenResult.IsEmpty || maCloseResult.IsEmpty || !_maOpenIndicator.IsFormed || !_maCloseIndicator.IsFormed)
return;
var maOpen = maOpenResult.ToDecimal();
var maClose = maCloseResult.ToDecimal();
EnqueueValue(_maOpenValues, maOpen, _maxOpenQueueSize);
EnqueueValue(_maCloseValues, maClose, _maxCloseQueueSize);
var maOpenPrev = GetQueueValue(_maOpenValues, 1 + MaOpenShift);
var maOpenSignal = GetQueueValue(_maOpenValues, MaOpenSignalBar + MaOpenShift);
var maClosePrev = GetQueueValue(_maCloseValues, 1 + MaCloseShift);
var maCloseSignal = GetQueueValue(_maCloseValues, MaCloseSignalBar + MaCloseShift);
if (maOpenPrev is null || maOpenSignal is null || maClosePrev is null || maCloseSignal is null)
return;
var longSignal = maOpenPrev > maOpenSignal && maClosePrev > maCloseSignal;
var shortSignal = maOpenPrev < maOpenSignal && maClosePrev < maCloseSignal;
if (Position == 0)
{
if (longSignal)
{
OpenLong(candle.ClosePrice);
}
else if (shortSignal)
{
OpenShort(candle.ClosePrice);
}
}
else
{
ManageOpenPosition(candle, maOpenPrev.Value, maOpenSignal.Value);
}
}
private void OpenLong(decimal price)
{
var volume = Volume;
if (volume <= 0m)
return;
_entryPrice = price;
_stopPrice = StopLossPips > 0m ? price - StopLossPips * _pipSize : null;
_takePrice = TakeProfitPips > 0m ? price + TakeProfitPips * _pipSize : null;
BuyMarket(volume);
}
private void OpenShort(decimal price)
{
var volume = Volume;
if (volume <= 0m)
return;
_entryPrice = price;
_stopPrice = StopLossPips > 0m ? price + StopLossPips * _pipSize : null;
_takePrice = TakeProfitPips > 0m ? price - TakeProfitPips * _pipSize : null;
SellMarket(volume);
}
private void ManageOpenPosition(ICandleMessage candle, decimal maOpenPrev, decimal maOpenSignal)
{
var position = Position;
if (position > 0)
{
if (maOpenPrev < maOpenSignal)
{
SellMarket(position);
ResetPositionState();
return;
}
UpdateTrailingForLong(candle);
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(position);
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(position);
ResetPositionState();
}
}
else if (position < 0)
{
if (maOpenPrev > maOpenSignal)
{
BuyMarket(-position);
ResetPositionState();
return;
}
UpdateTrailingForShort(candle);
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(-position);
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(-position);
ResetPositionState();
}
}
else
{
ResetPositionState();
}
}
private void UpdateTrailingForLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _entryPrice is null)
return;
var trailingStop = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
if (trailingStop <= 0m)
return;
var currentPrice = candle.ClosePrice;
var entryPrice = _entryPrice.Value;
if (currentPrice - entryPrice <= trailingStop + trailingStep)
return;
var threshold = currentPrice - (trailingStop + trailingStep);
if (_stopPrice.HasValue && _stopPrice.Value >= threshold)
return;
var newStop = currentPrice - trailingStop;
if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
_stopPrice = newStop;
}
private void UpdateTrailingForShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _entryPrice is null)
return;
var trailingStop = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
if (trailingStop <= 0m)
return;
var currentPrice = candle.ClosePrice;
var entryPrice = _entryPrice.Value;
if (entryPrice - currentPrice <= trailingStop + trailingStep)
return;
var threshold = currentPrice + trailingStop + trailingStep;
if (_stopPrice.HasValue && _stopPrice.Value <= threshold)
return;
var newStop = currentPrice + trailingStop;
if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
_stopPrice = newStop;
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
private void UpdatePipSize()
{
var step = Security?.PriceStep ?? 1m;
_pipSize = step;
}
private void UpdateQueueSizes()
{
var shiftOpen = Math.Max(0, MaOpenShift);
var shiftClose = Math.Max(0, MaCloseShift);
var openDepth = Math.Max(Math.Max(1 + shiftOpen, MaOpenSignalBar + shiftOpen), MaCloseSignalBar + shiftOpen);
var closeDepth = Math.Max(1 + shiftClose, 1);
_maxOpenQueueSize = Math.Max(2, openDepth + 2);
_maxCloseQueueSize = Math.Max(2, closeDepth + 2);
}
private static void EnqueueValue(List<decimal> queue, decimal value, int maxSize)
{
queue.Add(value);
while (queue.Count > maxSize)
queue.RemoveAt(0);
}
private static decimal? GetQueueValue(List<decimal> queue, int indexFromCurrent)
{
if (indexFromCurrent < 0)
return null;
if (queue.Count <= indexFromCurrent)
return null;
var targetIndex = queue.Count - 1 - indexFromCurrent;
return targetIndex >= 0 && targetIndex < queue.Count
? queue[targetIndex]
: null;
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
return method switch
{
MovingAverageMethods.Sma => new SimpleMovingAverage { Length = length },
MovingAverageMethods.Ema => new ExponentialMovingAverage { Length = length },
MovingAverageMethods.Smma => new SmoothedMovingAverage { Length = length },
MovingAverageMethods.Lwma => new WeightedMovingAverage { Length = length },
_ => new ExponentialMovingAverage { Length = length }
};
}
private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPriceTypes priceType)
{
return priceType switch
{
AppliedPriceTypes.Close => candle.ClosePrice,
AppliedPriceTypes.Open => candle.OpenPrice,
AppliedPriceTypes.High => candle.HighPrice,
AppliedPriceTypes.Low => candle.LowPrice,
AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + candle.ClosePrice + candle.ClosePrice) / 4m,
_ => candle.ClosePrice
};
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math, Decimal
from indicator_extensions import *
class brandy_strategy(Strategy):
def __init__(self):
super(brandy_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._take_profit_pips = self.Param("TakeProfitPips", 150.0)
self._trailing_stop_pips = self.Param("TrailingStopPips", 5.0)
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0)
self._ma_close_period = self.Param("MaClosePeriod", 20)
self._ma_close_shift = self.Param("MaCloseShift", 0)
self._ma_close_signal_bar = self.Param("MaCloseSignalBar", 0)
self._ma_open_period = self.Param("MaOpenPeriod", 70)
self._ma_open_shift = self.Param("MaOpenShift", 0)
self._ma_open_signal_bar = self.Param("MaOpenSignalBar", 0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._ma_open_indicator = None
self._ma_close_indicator = None
self._pip_size = 1.0
self._ma_open_values = []
self._ma_close_values = []
self._max_open_queue_size = 2
self._max_close_queue_size = 2
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(brandy_strategy, self).OnStarted2(time)
self._ma_open_indicator = ExponentialMovingAverage()
self._ma_open_indicator.Length = self._ma_open_period.Value
self._ma_close_indicator = ExponentialMovingAverage()
self._ma_close_indicator.Length = self._ma_close_period.Value
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
self._pip_size = step if step > 0 else 1.0
shift_open = max(0, self._ma_open_shift.Value)
shift_close = max(0, self._ma_close_shift.Value)
open_depth = max(max(1 + shift_open, self._ma_open_signal_bar.Value + shift_open),
self._ma_close_signal_bar.Value + shift_open)
close_depth = max(1 + shift_close, 1)
self._max_open_queue_size = max(2, open_depth + 2)
self._max_close_queue_size = max(2, close_depth + 2)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ma_open_indicator is None or self._ma_close_indicator is None:
return
open_source = float(candle.ClosePrice)
close_source = float(candle.ClosePrice)
ma_open_result = process_float(self._ma_open_indicator, Decimal(float(open_source)), candle.OpenTime, True)
ma_close_result = process_float(self._ma_close_indicator, Decimal(float(close_source)), candle.OpenTime, True)
if (ma_open_result.IsEmpty or ma_close_result.IsEmpty or
not self._ma_open_indicator.IsFormed or not self._ma_close_indicator.IsFormed):
return
ma_open = float(ma_open_result.Value)
ma_close = float(ma_close_result.Value)
self._enqueue_value(self._ma_open_values, ma_open, self._max_open_queue_size)
self._enqueue_value(self._ma_close_values, ma_close, self._max_close_queue_size)
ma_open_prev = self._get_queue_value(self._ma_open_values, 1 + self._ma_open_shift.Value)
ma_open_signal = self._get_queue_value(self._ma_open_values, self._ma_open_signal_bar.Value + self._ma_open_shift.Value)
ma_close_prev = self._get_queue_value(self._ma_close_values, 1 + self._ma_close_shift.Value)
ma_close_signal = self._get_queue_value(self._ma_close_values, self._ma_close_signal_bar.Value + self._ma_close_shift.Value)
if ma_open_prev is None or ma_open_signal is None or ma_close_prev is None or ma_close_signal is None:
return
long_signal = ma_open_prev > ma_open_signal and ma_close_prev > ma_close_signal
short_signal = ma_open_prev < ma_open_signal and ma_close_prev < ma_close_signal
if self.Position == 0:
if long_signal:
self._open_long(float(candle.ClosePrice))
elif short_signal:
self._open_short(float(candle.ClosePrice))
else:
self._manage_open_position(candle, ma_open_prev, ma_open_signal)
def _open_long(self, price):
self._entry_price = price
self._stop_price = price - self._stop_loss_pips.Value * self._pip_size if self._stop_loss_pips.Value > 0 else None
self._take_price = price + self._take_profit_pips.Value * self._pip_size if self._take_profit_pips.Value > 0 else None
self.BuyMarket()
def _open_short(self, price):
self._entry_price = price
self._stop_price = price + self._stop_loss_pips.Value * self._pip_size if self._stop_loss_pips.Value > 0 else None
self._take_price = price - self._take_profit_pips.Value * self._pip_size if self._take_profit_pips.Value > 0 else None
self.SellMarket()
def _manage_open_position(self, candle, ma_open_prev, ma_open_signal):
if self.Position > 0:
if ma_open_prev < ma_open_signal:
self.SellMarket(self.Position)
self._reset_position_state()
return
self._update_trailing_for_long(candle)
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket(self.Position)
self._reset_position_state()
return
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(self.Position)
self._reset_position_state()
elif self.Position < 0:
if ma_open_prev > ma_open_signal:
self.BuyMarket(abs(self.Position))
self._reset_position_state()
return
self._update_trailing_for_short(candle)
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket(abs(self.Position))
self._reset_position_state()
return
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_position_state()
else:
self._reset_position_state()
def _update_trailing_for_long(self, candle):
if self._trailing_stop_pips.Value <= 0 or self._trailing_step_pips.Value <= 0 or self._entry_price is None:
return
trailing_stop = self._trailing_stop_pips.Value * self._pip_size
trailing_step = self._trailing_step_pips.Value * self._pip_size
current_price = float(candle.ClosePrice)
if current_price - self._entry_price <= trailing_stop + trailing_step:
return
threshold = current_price - (trailing_stop + trailing_step)
if self._stop_price is not None and self._stop_price >= threshold:
return
new_stop = current_price - trailing_stop
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
def _update_trailing_for_short(self, candle):
if self._trailing_stop_pips.Value <= 0 or self._trailing_step_pips.Value <= 0 or self._entry_price is None:
return
trailing_stop = self._trailing_stop_pips.Value * self._pip_size
trailing_step = self._trailing_step_pips.Value * self._pip_size
current_price = float(candle.ClosePrice)
if self._entry_price - current_price <= trailing_stop + trailing_step:
return
threshold = current_price + trailing_stop + trailing_step
if self._stop_price is not None and self._stop_price <= threshold:
return
new_stop = current_price + trailing_stop
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def _enqueue_value(self, queue, value, max_size):
queue.append(value)
while len(queue) > max_size:
queue.pop(0)
def _get_queue_value(self, queue, index_from_current):
if index_from_current < 0:
return None
if len(queue) <= index_from_current:
return None
target = len(queue) - 1 - index_from_current
if target >= 0 and target < len(queue):
return queue[target]
return None
def OnReseted(self):
super(brandy_strategy, self).OnReseted()
self._ma_open_indicator = None
self._ma_close_indicator = None
self._pip_size = 1.0
self._ma_open_values = []
self._ma_close_values = []
self._entry_price = None
self._stop_price = None
self._take_price = None
def CreateClone(self):
return brandy_strategy()