Стратегия Starter
Starter — порт оригинального советника MetaTrader 5 «Starter (barabashkakvn's edition)». Алгоритм отслеживает выход индекса товарного канала (CCI) из зоны перепроданности или перекупленности и подтверждает сигнал наклоном длинной скользящей средней. Когда импульс совпадает с направлением трендового фильтра, стратегия открывает одну рыночную позицию с объёмом, рассчитанным по доле риска от капитала. Стоп-лосс и ступенчатое сопровождение воспроизводят правила управления капиталом исходного эксперта.
Логика торговли
- Трендовый фильтр — настраиваемая скользящая средняя должна расти быстрее
MaDelta, чтобы разрешить покупки, и падать быстрееMaDelta, чтобы разрешить продажи. Поддерживаются те же методы сглаживания, что и в MQL (простая, экспоненциальная, сглаженная, линейно-взвешенная). - Подтверждение CCI — CCI анализируется только на закрытых свечах. Пересечение уровня
-CciLevelснизу вверх формирует длинный сигнал, пересечениеCciLevelсверху вниз — короткий. - Одна позиция — стратегия не открывает новых сделок, пока не закроет текущую, что отражает фильтрацию по символу и magic number в оригинале.
Условия входа
- Дождаться закрытия свечи.
- Получить значения скользящей средней на текущем и предыдущем сдвиге.
- Получить значения CCI на текущем и предыдущем сдвиге.
- Покупка выполняется, если:
- Разница между текущей и предыдущей MA превышает
MaDelta. - Текущий CCI выше предыдущего.
- CCI пересекает
-CciLevelснизу вверх.
- Разница между текущей и предыдущей MA превышает
- Продажа выполняется, если:
- Разница между MA меньше
-MaDelta. - Текущий CCI ниже предыдущего.
- CCI пересекает
CciLevelсверху вниз.
- Разница между MA меньше
Условия выхода
- Первичный стоп-лосс — при
StopLossPips > 0стоп рассчитывается как цена входа ±StopLossPips * PriceStep. - Трейлинг-стоп — если заданы
TrailingStopPipsиTrailingStepPips, стоп передвигается всякий раз, когда цена улучшается минимум на заданный шаг. Для лонга стоп =Close - TrailingStop, для шорта =Close + TrailingStop. - Принудительное закрытие — при достижении уровня стопа внутри свечи позиция закрывается рыночной заявкой, а защитные уровни сбрасываются.
Управление рисками
- Размер позиции — базовый объём равен
Portfolio.CurrentValue * MaximumRisk / price. Если значение капитала недоступно, используется свойствоVolume(по умолчанию 1). - Снижение объёма после убытков — после двух и более убыточных сделок подряд объём уменьшается на
volume * losses / DecreaseFactor. Любая прибыльная сделка обнуляет счётчик.
Параметры
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
MaximumRisk |
0.02 |
Доля капитала, которая рискуется в одной сделке. |
DecreaseFactor |
3 |
Коэффициент уменьшения объёма после серии убытков. |
CciPeriod |
14 |
Период расчёта CCI. |
CciLevel |
100 |
Порог перепроданности/перекупленности для CCI. |
CciCurrentBar |
0 |
Сдвиг текущего значения CCI. |
CciPreviousBar |
1 |
Сдвиг предыдущего значения CCI. |
MaPeriod |
120 |
Период трендовой скользящей средней. |
MaMethod |
Simple |
Метод сглаживания MA (Simple, Exponential, Smoothed, LinearWeighted). |
MaCurrentBar |
0 |
Сдвиг скользящей средней. |
MaDelta |
0.001 |
Минимальный прирост наклона между текущей и предыдущей MA. |
StopLossPips |
0 |
Дистанция первичного стоп-лосса в пунктах. |
TrailingStopPips |
5 |
Базовая дистанция трейлинг-стопа в пунктах. |
TrailingStepPips |
5 |
Минимальное улучшение цены для передвижения трейлинг-стопа. |
CandleType |
таймфрейм 30m |
Тип свечей, используемый стратегией. |
Особенности реализации
- Значения индикаторов кэшируются для доступа по сдвигам, что соответствует работе с буферами в MQL.
- Размер пункта вычисляется из
Security.PriceStep; если биржа не предоставляет корректный шаг цены, защитные приказы отключаются. - Все комментарии в коде написаны на английском языке согласно требованиям репозитория.
- Python-версия намеренно отсутствует по просьбе задания.
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 MetaTrader 5 expert advisor "Starter".
/// Uses the Commodity Channel Index and a moving average slope filter to open trades with adaptive position sizing and trailing protection.
/// </summary>
public class StarterStrategy : Strategy
{
private readonly StrategyParam<decimal> _maximumRisk;
private readonly StrategyParam<decimal> _decreaseFactor;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<decimal> _cciLevel;
private readonly StrategyParam<int> _cciCurrentBar;
private readonly StrategyParam<int> _cciPreviousBar;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<MovingAverageMethods> _maMethod;
private readonly StrategyParam<int> _maCurrentBar;
private readonly StrategyParam<decimal> _maDelta;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private CommodityChannelIndex _cci = null!;
private DecimalLengthIndicator _movingAverage = null!;
private readonly List<decimal> _cciHistory = new();
private readonly List<decimal> _maHistory = new();
private decimal _pipSize;
private int _historyCapacity;
private decimal? _longEntryPrice;
private decimal? _shortEntryPrice;
private decimal? _longStop;
private decimal? _shortStop;
private decimal _signedPosition;
private Sides? _lastEntrySide;
private decimal _lastEntryPrice;
private int _consecutiveLosses;
/// <summary>
/// Initializes a new instance of the <see cref="StarterStrategy"/> class.
/// </summary>
public StarterStrategy()
{
_maximumRisk = Param(nameof(MaximumRisk), 0.02m)
.SetNotNegative()
.SetDisplay("Maximum Risk", "Fraction of portfolio equity risked per trade", "Risk Management");
_decreaseFactor = Param(nameof(DecreaseFactor), 3m)
.SetNotNegative()
.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management");
_cciPeriod = Param(nameof(CciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Number of bars for the Commodity Channel Index", "Indicators")
.SetOptimize(5, 60, 1);
_cciLevel = Param(nameof(CciLevel), 100m)
.SetGreaterThanZero()
.SetDisplay("CCI Level", "Threshold used for oversold/overbought detection", "Indicators")
.SetOptimize(50m, 200m, 10m);
_cciCurrentBar = Param(nameof(CciCurrentBar), 0)
.SetNotNegative()
.SetDisplay("CCI Current Bar", "Shift for the current CCI value", "Indicators");
_cciPreviousBar = Param(nameof(CciPreviousBar), 1)
.SetNotNegative()
.SetDisplay("CCI Previous Bar", "Shift for the previous CCI value", "Indicators");
_maPeriod = Param(nameof(MaPeriod), 120)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Number of bars for the moving average", "Indicators")
.SetOptimize(20, 200, 5);
_maMethod = Param(nameof(MaMethod), MovingAverageMethods.Simple)
.SetDisplay("MA Method", "Smoothing method applied to the moving average", "Indicators");
_maCurrentBar = Param(nameof(MaCurrentBar), 0)
.SetNotNegative()
.SetDisplay("MA Current Bar", "Shift for the moving average", "Indicators");
_maDelta = Param(nameof(MaDelta), 0.001m)
.SetNotNegative()
.SetDisplay("MA Delta", "Minimum slope difference between current and previous MA", "Signals")
.SetOptimize(0.0001m, 0.01m, 0.0001m);
_stopLossPips = Param(nameof(StopLossPips), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Initial protective stop distance in pips", "Risk Management")
.SetOptimize(0m, 200m, 10m);
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Base trailing distance in pips", "Risk Management")
.SetOptimize(0m, 200m, 5m);
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Minimum improvement required before moving the trailing stop", "Risk Management")
.SetOptimize(0m, 200m, 5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe processed by the strategy", "General");
}
/// <summary>
/// Risk per trade expressed as a fraction of portfolio equity.
/// </summary>
public decimal MaximumRisk
{
get => _maximumRisk.Value;
set => _maximumRisk.Value = value;
}
/// <summary>
/// Lot reduction factor applied after consecutive losing trades.
/// </summary>
public decimal DecreaseFactor
{
get => _decreaseFactor.Value;
set => _decreaseFactor.Value = value;
}
/// <summary>
/// Period for the Commodity Channel Index indicator.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Overbought/oversold CCI threshold.
/// </summary>
public decimal CciLevel
{
get => _cciLevel.Value;
set => _cciLevel.Value = value;
}
/// <summary>
/// Index of the bar considered "current" for CCI comparisons.
/// </summary>
public int CciCurrentBar
{
get => _cciCurrentBar.Value;
set => _cciCurrentBar.Value = value;
}
/// <summary>
/// Index of the bar considered "previous" for CCI comparisons.
/// </summary>
public int CciPreviousBar
{
get => _cciPreviousBar.Value;
set => _cciPreviousBar.Value = value;
}
/// <summary>
/// Period for the trend filter moving average.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Moving average smoothing method.
/// </summary>
public MovingAverageMethods MaMethod
{
get => _maMethod.Value;
set => _maMethod.Value = value;
}
/// <summary>
/// Shift for the moving average value considered "current".
/// </summary>
public int MaCurrentBar
{
get => _maCurrentBar.Value;
set => _maCurrentBar.Value = value;
}
/// <summary>
/// Minimum slope difference between current and previous moving average values.
/// </summary>
public decimal MaDelta
{
get => _maDelta.Value;
set => _maDelta.Value = value;
}
/// <summary>
/// Initial stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum improvement before advancing the trailing stop in pips.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Candle data type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cci?.Reset();
_movingAverage?.Reset();
_cci = null!;
_movingAverage = null!;
_cciHistory.Clear();
_maHistory.Clear();
_pipSize = 0m;
_historyCapacity = 0;
_longEntryPrice = null;
_shortEntryPrice = null;
_longStop = null;
_shortStop = null;
_signedPosition = 0m;
_lastEntrySide = null;
_lastEntryPrice = 0m;
_consecutiveLosses = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = GetPipSize();
_historyCapacity = CalculateHistoryCapacity();
_cciHistory.Clear();
_maHistory.Clear();
_longEntryPrice = null;
_shortEntryPrice = null;
_longStop = null;
_shortStop = null;
_signedPosition = 0m;
_lastEntrySide = null;
_lastEntryPrice = 0m;
_consecutiveLosses = 0;
_cci = new CommodityChannelIndex { Length = CciPeriod };
_movingAverage = CreateMovingAverage(MaMethod, MaPeriod);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_cci, _movingAverage, OnProcessCandle)
.Start();
}
private void OnProcessCandle(ICandleMessage candle, decimal cciValue, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_cci.IsFormed || !_movingAverage.IsFormed)
return;
// Store the latest indicator values so we can access shifted history like in MetaTrader.
AddHistory(_cciHistory, cciValue);
AddHistory(_maHistory, maValue);
if (Position != 0)
{
// Manage trailing stop and protective exits for open positions before evaluating new entries.
UpdateTrailing(candle);
CheckProtectiveStops(candle);
}
if (Position != 0)
// The original expert only opens a new position when no trades are active.
return;
if (!TryGetHistoryValue(_maHistory, MaCurrentBar, out var maCurrent) ||
!TryGetHistoryValue(_maHistory, MaCurrentBar + 1, out var maPrevious))
return;
if (!TryGetHistoryValue(_cciHistory, CciCurrentBar, out var cciCurrent) ||
!TryGetHistoryValue(_cciHistory, CciPreviousBar, out var cciPrevious))
return;
// Compare the moving average slope and CCI swings to detect breakout conditions.
var maSlope = maCurrent - maPrevious;
if (maSlope > MaDelta && cciCurrent > cciPrevious &&
cciCurrent > -CciLevel && cciPrevious < -CciLevel)
{
TryEnterLong(candle.ClosePrice);
}
else if (maSlope < -MaDelta && cciCurrent < cciPrevious &&
cciCurrent < CciLevel && cciPrevious > CciLevel)
{
TryEnterShort(candle.ClosePrice);
}
}
private void TryEnterLong(decimal price)
{
var volume = CalculateTradeVolume(price);
if (volume <= 0m)
return;
BuyMarket(volume);
LogInfo($"Opening long position at {price} with volume {volume}.");
}
private void TryEnterShort(decimal price)
{
var volume = CalculateTradeVolume(price);
if (volume <= 0m)
return;
SellMarket(volume);
LogInfo($"Opening short position at {price} with volume {volume}.");
}
private void CheckProtectiveStops(ICandleMessage candle)
{
if (Position > 0 && _longStop.HasValue && candle.LowPrice <= _longStop.Value)
{
var volume = Math.Abs(Position);
if (volume > 0)
{
SellMarket(volume);
LogInfo($"Long stop-loss triggered at {_longStop.Value}.");
}
ResetLongProtection();
return;
}
if (Position < 0 && _shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
{
var volume = Math.Abs(Position);
if (volume > 0)
{
BuyMarket(volume);
LogInfo($"Short stop-loss triggered at {_shortStop.Value}.");
}
ResetShortProtection();
}
}
private void UpdateTrailing(ICandleMessage candle)
{
if (TrailingStopPips <= 0m || _pipSize <= 0m)
return;
var offset = TrailingStopPips * _pipSize;
var step = TrailingStepPips * _pipSize;
if (Position > 0 && _longEntryPrice.HasValue)
{
// Advance the long stop only when price improves by at least the configured step.
var targetStop = candle.ClosePrice - offset;
var threshold = candle.ClosePrice - (offset + step);
if (!_longStop.HasValue || _longStop.Value < threshold)
{
_longStop = targetStop;
LogInfo($"Trailing long stop moved to {_longStop.Value}.");
}
}
else if (Position < 0 && _shortEntryPrice.HasValue)
{
// Mirror the trailing logic for short positions.
var targetStop = candle.ClosePrice + offset;
var threshold = candle.ClosePrice + (offset + step);
if (!_shortStop.HasValue || _shortStop.Value > threshold)
{
_shortStop = targetStop;
LogInfo($"Trailing short stop moved to {_shortStop.Value}.");
}
}
}
private decimal CalculateTradeVolume(decimal price)
{
// Start from the configured strategy volume; fall back to 1 if undefined.
var baseVolume = Volume > 0 ? Volume : 1m;
if (price <= 0m)
return NormalizeVolume(baseVolume);
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity <= 0m || MaximumRisk <= 0m)
return NormalizeVolume(baseVolume);
// Position size equals equity * risk percent divided by price, mimicking the original risk formula.
var volume = equity * MaximumRisk / price;
if (DecreaseFactor > 0m && _consecutiveLosses > 1)
{
// Reduce the lot size after two or more losses, replicating MetaTrader's "DecreaseFactor" behavior.
var reduction = volume * _consecutiveLosses / DecreaseFactor;
volume -= reduction;
}
if (volume <= 0m)
volume = baseVolume;
return NormalizeVolume(volume);
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security != null)
{
var step = security.VolumeStep ?? 1m;
if (step <= 0m)
step = 1m;
if (volume < step)
volume = step;
var steps = Math.Floor(volume / step);
if (steps < 1m)
steps = 1m;
volume = steps * step;
}
if (volume <= 0m)
volume = 1m;
return volume;
}
private void AddHistory(List<decimal> history, decimal value)
{
history.Add(value);
if (history.Count > _historyCapacity)
history.RemoveRange(0, history.Count - _historyCapacity);
}
private static bool TryGetHistoryValue(List<decimal> history, int shift, out decimal value)
{
value = default;
if (shift < 0)
return false;
var index = history.Count - 1 - shift;
if (index < 0 || index >= history.Count)
return false;
value = history[index];
return true;
}
private void ResetLongProtection()
{
_longEntryPrice = null;
_longStop = null;
}
private void ResetShortProtection()
{
_shortEntryPrice = null;
_shortStop = null;
}
private decimal GetPipSize()
{
var security = Security;
if (security == null)
return 0m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
return step;
}
private int CalculateHistoryCapacity()
{
var cciRequirement = Math.Max(CciCurrentBar, CciPreviousBar) + CciPeriod + 5;
var maRequirement = MaCurrentBar + MaPeriod + 5;
return Math.Max(cciRequirement, maRequirement);
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int period)
{
return method switch
{
MovingAverageMethods.Simple => new SMA { Length = period },
MovingAverageMethods.Exponential => new EMA { Length = period },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
_ => new SMA { Length = period }
};
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
// Track executed volume to update position state and the loss streak counter.
var volume = trade.Trade.Volume;
if (volume <= 0m)
return;
var delta = trade.Order.Side == Sides.Buy ? volume : -volume;
var previousPosition = _signedPosition;
_signedPosition += delta;
if (previousPosition == 0m && _signedPosition != 0m)
{
_lastEntrySide = trade.Order.Side;
_lastEntryPrice = trade.Trade.Price;
if (_lastEntrySide == Sides.Buy)
{
_longEntryPrice = trade.Trade.Price;
_longStop = StopLossPips > 0m && _pipSize > 0m ? _lastEntryPrice - (StopLossPips * _pipSize) : null;
ResetShortProtection();
}
else if (_lastEntrySide == Sides.Sell)
{
_shortEntryPrice = trade.Trade.Price;
_shortStop = StopLossPips > 0m && _pipSize > 0m ? _lastEntryPrice + (StopLossPips * _pipSize) : null;
ResetLongProtection();
}
}
else if (previousPosition != 0m && _signedPosition == 0m)
{
var exitPrice = trade.Trade.Price;
if (_lastEntrySide != null && _lastEntryPrice != 0m)
{
var profit = _lastEntrySide == Sides.Buy
? exitPrice - _lastEntryPrice
: _lastEntryPrice - exitPrice;
if (profit > 0m)
{
_consecutiveLosses = 0;
}
else if (profit < 0m)
{
_consecutiveLosses++;
}
}
_lastEntrySide = null;
_lastEntryPrice = 0m;
ResetLongProtection();
ResetShortProtection();
}
}
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average (equivalent to MODE_SMA in MetaTrader).
/// </summary>
Simple,
/// <summary>
/// Exponential moving average (MODE_EMA).
/// </summary>
Exponential,
/// <summary>
/// Smoothed moving average (MODE_SMMA).
/// </summary>
Smoothed,
/// <summary>
/// Linear weighted moving average (MODE_LWMA).
/// </summary>
LinearWeighted
}
}
import clr
import math
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, Sides
from StockSharp.Algo.Indicators import (
CommodityChannelIndex, SimpleMovingAverage,
ExponentialMovingAverage, SmoothedMovingAverage,
WeightedMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
class starter_strategy(Strategy):
def __init__(self):
super(starter_strategy, self).__init__()
self._maximum_risk = self.Param("MaximumRisk", 0.02) \
.SetDisplay("Maximum Risk", "Fraction of portfolio equity risked per trade", "Risk Management")
self._decrease_factor = self.Param("DecreaseFactor", 3.0) \
.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management")
self._cci_period = self.Param("CciPeriod", 14) \
.SetDisplay("CCI Period", "Number of bars for the Commodity Channel Index", "Indicators")
self._cci_level = self.Param("CciLevel", 100.0) \
.SetDisplay("CCI Level", "Threshold used for oversold/overbought detection", "Indicators")
self._cci_current_bar = self.Param("CciCurrentBar", 0) \
.SetDisplay("CCI Current Bar", "Shift for the current CCI value", "Indicators")
self._cci_previous_bar = self.Param("CciPreviousBar", 1) \
.SetDisplay("CCI Previous Bar", "Shift for the previous CCI value", "Indicators")
self._ma_period = self.Param("MaPeriod", 120) \
.SetDisplay("MA Period", "Number of bars for the moving average", "Indicators")
self._ma_current_bar = self.Param("MaCurrentBar", 0) \
.SetDisplay("MA Current Bar", "Shift for the moving average", "Indicators")
self._ma_delta = self.Param("MaDelta", 0.001) \
.SetDisplay("MA Delta", "Minimum slope difference between current and previous MA", "Signals")
self._stop_loss_pips = self.Param("StopLossPips", 0.0) \
.SetDisplay("Stop Loss (pips)", "Initial protective stop distance in pips", "Risk Management")
self._trailing_stop_pips = self.Param("TrailingStopPips", 5.0) \
.SetDisplay("Trailing Stop (pips)", "Base trailing distance in pips", "Risk Management")
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0) \
.SetDisplay("Trailing Step (pips)", "Minimum improvement required before moving the trailing stop", "Risk Management")
self._cci = None
self._moving_average = None
self._cci_history = []
self._ma_history = []
self._pip_size = 0.0
self._history_capacity = 0
self._long_entry_price = None
self._short_entry_price = None
self._long_stop = None
self._short_stop = None
self._signed_position = 0.0
self._last_entry_side = None
self._last_entry_price = 0.0
self._consecutive_losses = 0
@property
def maximum_risk(self):
return self._maximum_risk.Value
@property
def decrease_factor(self):
return self._decrease_factor.Value
@property
def cci_period(self):
return self._cci_period.Value
@property
def cci_level(self):
return self._cci_level.Value
@property
def cci_current_bar(self):
return self._cci_current_bar.Value
@property
def cci_previous_bar(self):
return self._cci_previous_bar.Value
@property
def ma_period(self):
return self._ma_period.Value
@property
def ma_current_bar(self):
return self._ma_current_bar.Value
@property
def ma_delta(self):
return self._ma_delta.Value
@property
def stop_loss_pips(self):
return self._stop_loss_pips.Value
@property
def trailing_stop_pips(self):
return self._trailing_stop_pips.Value
@property
def trailing_step_pips(self):
return self._trailing_step_pips.Value
def OnReseted(self):
super(starter_strategy, self).OnReseted()
self._cci = None
self._moving_average = None
self._cci_history = []
self._ma_history = []
self._pip_size = 0.0
self._history_capacity = 0
self._long_entry_price = None
self._short_entry_price = None
self._long_stop = None
self._short_stop = None
self._signed_position = 0.0
self._last_entry_side = None
self._last_entry_price = 0.0
self._consecutive_losses = 0
def OnStarted2(self, time):
super(starter_strategy, self).OnStarted2(time)
self._pip_size = self._get_pip_size()
self._history_capacity = self._calc_history_capacity()
self._cci_history = []
self._ma_history = []
self._long_entry_price = None
self._short_entry_price = None
self._long_stop = None
self._short_stop = None
self._signed_position = 0.0
self._last_entry_side = None
self._last_entry_price = 0.0
self._consecutive_losses = 0
self._cci = CommodityChannelIndex()
self._cci.Length = self.cci_period
self._moving_average = SimpleMovingAverage()
self._moving_average.Length = self.ma_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
subscription.Bind(self._cci, self._moving_average, self._on_process_candle)
subscription.Start()
def _on_process_candle(self, candle, cci_value, ma_value):
if candle.State != CandleStates.Finished:
return
if not self._cci.IsFormed or not self._moving_average.IsFormed:
return
cci_val = float(cci_value)
ma_val = float(ma_value)
self._add_history(self._cci_history, cci_val)
self._add_history(self._ma_history, ma_val)
if self.Position != 0:
self._update_trailing(candle)
self._check_protective_stops(candle)
if self.Position != 0:
return
ma_current = self._try_get_history(self._ma_history, self.ma_current_bar)
ma_previous = self._try_get_history(self._ma_history, self.ma_current_bar + 1)
if ma_current is None or ma_previous is None:
return
cci_current = self._try_get_history(self._cci_history, self.cci_current_bar)
cci_previous = self._try_get_history(self._cci_history, self.cci_previous_bar)
if cci_current is None or cci_previous is None:
return
ma_slope = ma_current - ma_previous
cci_lev = float(self.cci_level)
ma_d = float(self.ma_delta)
if (ma_slope > ma_d and cci_current > cci_previous and
cci_current > -cci_lev and cci_previous < -cci_lev):
self._try_enter_long(float(candle.ClosePrice))
elif (ma_slope < -ma_d and cci_current < cci_previous and
cci_current < cci_lev and cci_previous > cci_lev):
self._try_enter_short(float(candle.ClosePrice))
def _try_enter_long(self, price):
volume = self._calculate_trade_volume(price)
if volume <= 0.0:
return
self.BuyMarket(volume)
def _try_enter_short(self, price):
volume = self._calculate_trade_volume(price)
if volume <= 0.0:
return
self.SellMarket(volume)
def _check_protective_stops(self, candle):
if self.Position > 0 and self._long_stop is not None and float(candle.LowPrice) <= self._long_stop:
vol = abs(float(self.Position))
if vol > 0:
self.SellMarket(vol)
self._reset_long_protection()
return
if self.Position < 0 and self._short_stop is not None and float(candle.HighPrice) >= self._short_stop:
vol = abs(float(self.Position))
if vol > 0:
self.BuyMarket(vol)
self._reset_short_protection()
def _update_trailing(self, candle):
trail_pips = float(self.trailing_stop_pips)
if trail_pips <= 0.0 or self._pip_size <= 0.0:
return
offset = trail_pips * self._pip_size
step_pips = float(self.trailing_step_pips)
step = step_pips * self._pip_size
close = float(candle.ClosePrice)
if self.Position > 0 and self._long_entry_price is not None:
target_stop = close - offset
threshold = close - (offset + step)
if self._long_stop is None or self._long_stop < threshold:
self._long_stop = target_stop
elif self.Position < 0 and self._short_entry_price is not None:
target_stop = close + offset
threshold = close + (offset + step)
if self._short_stop is None or self._short_stop > threshold:
self._short_stop = target_stop
def _calculate_trade_volume(self, price):
base_volume = float(self.Volume) if self.Volume > 0 else 1.0
if price <= 0.0:
return self._normalize_volume(base_volume)
equity = float(self.Portfolio.CurrentValue) if self.Portfolio is not None and self.Portfolio.CurrentValue is not None else 0.0
max_risk = float(self.maximum_risk)
if equity <= 0.0 or max_risk <= 0.0:
return self._normalize_volume(base_volume)
volume = equity * max_risk / price
dec_factor = float(self.decrease_factor)
if dec_factor > 0.0 and self._consecutive_losses > 1:
reduction = volume * self._consecutive_losses / dec_factor
volume -= reduction
if volume <= 0.0:
volume = base_volume
return self._normalize_volume(volume)
def _normalize_volume(self, volume):
if self.Security is not None and self.Security.VolumeStep is not None:
step = float(self.Security.VolumeStep)
if step <= 0.0:
step = 1.0
if volume < step:
volume = step
steps = math.floor(volume / step)
if steps < 1.0:
steps = 1.0
volume = steps * step
if volume <= 0.0:
volume = 1.0
return volume
def _add_history(self, history, value):
history.append(value)
if len(history) > self._history_capacity:
del history[:len(history) - self._history_capacity]
def _try_get_history(self, history, shift):
if shift < 0:
return None
index = len(history) - 1 - shift
if index < 0 or index >= len(history):
return None
return history[index]
def _reset_long_protection(self):
self._long_entry_price = None
self._long_stop = None
def _reset_short_protection(self):
self._short_entry_price = None
self._short_stop = None
def _get_pip_size(self):
if self.Security is None:
return 0.0
step = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 0.0
if step <= 0.0:
return 0.0
return step
def _calc_history_capacity(self):
cci_req = max(self.cci_current_bar, self.cci_previous_bar) + self.cci_period + 5
ma_req = self.ma_current_bar + self.ma_period + 5
return max(cci_req, ma_req)
def OnOwnTradeReceived(self, trade):
super(starter_strategy, self).OnOwnTradeReceived(trade)
volume = float(trade.Trade.Volume)
if volume <= 0.0:
return
delta = volume if trade.Order.Side == Sides.Buy else -volume
previous_position = self._signed_position
self._signed_position += delta
if previous_position == 0.0 and self._signed_position != 0.0:
self._last_entry_side = trade.Order.Side
self._last_entry_price = float(trade.Trade.Price)
sl_pips = float(self.stop_loss_pips)
if self._last_entry_side == Sides.Buy:
self._long_entry_price = self._last_entry_price
self._long_stop = (self._last_entry_price - sl_pips * self._pip_size) if (sl_pips > 0.0 and self._pip_size > 0.0) else None
self._reset_short_protection()
else:
self._short_entry_price = self._last_entry_price
self._short_stop = (self._last_entry_price + sl_pips * self._pip_size) if (sl_pips > 0.0 and self._pip_size > 0.0) else None
self._reset_long_protection()
elif previous_position != 0.0 and self._signed_position == 0.0:
exit_price = float(trade.Trade.Price)
if self._last_entry_side is not None and self._last_entry_price != 0.0:
if self._last_entry_side == Sides.Buy:
profit = exit_price - self._last_entry_price
else:
profit = self._last_entry_price - exit_price
if profit > 0.0:
self._consecutive_losses = 0
elif profit < 0.0:
self._consecutive_losses += 1
self._last_entry_side = None
self._last_entry_price = 0.0
self._reset_long_protection()
self._reset_short_protection()
def CreateClone(self):
return starter_strategy()