Стратегия Crypto Analysis
Обзор
Это порт советника MetaTrader 4 «Crypto Analysis» на StockSharp. Алгоритм отслеживает пробои после касания ценой внешней полосы Боллинджера на рабочем таймфрейме при сохранении нисходящей структуры (быстрая LWMA ниже медленной). Сделки разрешаются только при согласии импульса старшего таймфрейма и MACD на ещё более старшем таймфрейме. Управление позицией включает те же уровни защиты, что и в исходном советнике: стопы по пунктам, денежный трейлинг, перевод в безубыток и контроль просадки портфеля.
Логика торговли
- Рабочий таймфрейм: задаётся параметром (по умолчанию M15). Все правила входа/выхода оцениваются на этих свечах.
- Триггер волатильности: минимум предыдущей свечи должен коснуться или пробить нижнюю полосу Боллинджера (20, 2) для подготовки лонга; максимум — верхнюю полосу для шорта.
- Фильтр тренда: быстрая LWMA (по умолчанию 6) должна находиться ниже медленной LWMA (85), что повторяет проверку «медвежьего» уклона в MQL-коде.
- Подтверждение RSI: RSI(14) выше 50 для покупок и ниже 50 для продаж.
- Импульс: максимальное модульное отклонение трёх последних значений Momentum(14) со старшего таймфрейма от уровня 100 должно превышать порог для покупок/продаж.
- MACD старшего таймфрейма: MACD (12, 26, 9) считается по отдельным свечам старшего таймфрейма (по умолчанию H4); для лонга требуется MACD выше сигнальной линии, для шорта — ниже.
- Исполнение: при совпадении всех фильтров открывается рыночная заявка. Противоположная позиция закрывается, чтобы стратегия всегда оставалась в одном нетто-направлении, как и оригинальный советник.
Управление позицией
- Первичные уровни: расстояния стоп-лосса и тейк-профита в пунктах пересчитываются из шага цены с учётом 5- и 3-значных котировок (
0.00001и0.001умножаются на 10). - Трейлинг-стоп: после обновления максимума/минимума стоп подтягивается на расстояние
TrailingStopPips, причём сохраняется лучший уровень. - Безубыток: при достижении
BreakEvenTriggerPipsстоп переносится на цену входа плюс/минусBreakEvenOffsetPips. - Денежные цели: абсолютные и процентные цели закрывают позицию, как только плавающая прибыль достигает заданных уровней.
- Денежный трейлинг: после превышения
MoneyTrailTargetфиксируется максимум прибыли; при откате наMoneyTrailStopпозиция закрывается. - Эквити-стоп: отслеживается текущее значение портфеля плюс плавающий результат; если просадка от пика превышает
EquityRiskPercent, позиция закрывается.
Многотаймфреймовые данные
Стратегия автоматически подписывается на три источника данных:
- Основной ряд свечей для правил Боллинджера/LWMA/RSI.
- Старший таймфрейм для фильтра Momentum (по умолчанию H1).
- Свечи старшего таймфрейма для подтверждения MACD (по умолчанию H4).
Параметры
| Параметр | Описание |
|---|---|
OrderVolume |
Базовый объём сделки. Перед сменой направления противоположная позиция закрывается. |
UseMoneyTakeProfit |
Включает денежный тейк-профит. |
MoneyTakeProfit |
Прибыль в валюте портфеля, при достижении которой позиция закрывается. |
UsePercentTakeProfit |
Включает процентный тейк-профит от начального капитала. |
PercentTakeProfit |
Процент прибыли, необходимый для закрытия позиции при активном процентном таргете. |
EnableMoneyTrailing |
Активирует денежный трейлинг. |
MoneyTrailTarget |
Порог прибыли для запуска трейлинга. |
MoneyTrailStop |
Допустимый откат прибыли после активации трейлинга. |
StopLossPips |
Стоп-лосс в пунктах. |
TakeProfitPips |
Тейк-профит в пунктах. |
TrailingStopPips |
Дистанция трейлинг-стопа в пунктах. |
UseBreakEven |
Включает перевод в безубыток. |
BreakEvenTriggerPips |
Количество пунктов прибыли перед переносом стопа. |
BreakEvenOffsetPips |
Дополнительные пункты к цене входа при переносе стопа. |
FastMaPeriod |
Длина быстрой LWMA по типичной цене. |
SlowMaPeriod |
Длина медленной LWMA по типичной цене. |
MomentumPeriod |
Период Momentum на старшем таймфрейме. |
MomentumBuyThreshold |
Минимальное отклонение Momentum для покупок. |
MomentumSellThreshold |
Минимальное отклонение Momentum для продаж. |
MacdFastLength |
Быстрая EMA в MACD фильтре. |
MacdSlowLength |
Медленная EMA в MACD фильтре. |
MacdSignalLength |
Сигнальная EMA в MACD фильтре. |
UseEquityStop |
Включает защиту по просадке портфеля. |
EquityRiskPercent |
Допустимый процент просадки капитала до принудительного закрытия. |
CandleType |
Основной таймфрейм. |
MomentumCandleType |
Таймфрейм для Momentum. |
MacdCandleType |
Таймфрейм для MACD. |
Примечания
- Порт StockSharp поддерживает только одну нетто-позицию, аналогично оригинальному советнику, который закрывал противоположные ордера перед открытием новых.
- Все правила защиты рассчитываются на закрытии свечи, повторяя обработку «нового бара» из MQL.
- Для инструментов с нестандартным шагом цены скорректируйте параметры в пунктах под спецификацию биржи.
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Crypto Analysis: Bollinger/LWMA/RSI entry with higher-timeframe Momentum and MACD filters.
/// </summary>
public class CryptoAnalysisStrategy : Strategy
{
private const int _rsiLength = 14;
private const int _bandLength = 20;
private const decimal _bandWidth = 2m;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<bool> _useMoneyTakeProfit;
private readonly StrategyParam<decimal> _moneyTakeProfit;
private readonly StrategyParam<bool> _usePercentTakeProfit;
private readonly StrategyParam<decimal> _percentTakeProfit;
private readonly StrategyParam<bool> _enableMoneyTrailing;
private readonly StrategyParam<decimal> _moneyTrailTarget;
private readonly StrategyParam<decimal> _moneyTrailStop;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<bool> _useBreakEven;
private readonly StrategyParam<int> _breakEvenTriggerPips;
private readonly StrategyParam<int> _breakEvenOffsetPips;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<decimal> _momentumBuyThreshold;
private readonly StrategyParam<decimal> _momentumSellThreshold;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _macdSignalLength;
private readonly StrategyParam<bool> _useEquityStop;
private readonly StrategyParam<decimal> _equityRiskPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _momentumCandleType;
private readonly StrategyParam<DataType> _macdCandleType;
private readonly RelativeStrengthIndex _rsi = new() { Length = _rsiLength };
private readonly List<Bar> _primary = [];
private readonly List<decimal> _momentumCloses = [];
private readonly Queue<decimal> _momentumDeviations = new();
private decimal? _macdFast;
private decimal? _macdSlow;
private decimal? _macdSignal;
private bool _macdReady;
private int _macdCount;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _bestPrice;
private decimal _initialEquity;
private decimal _peakEquity;
private decimal? _moneyTrailPeak;
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public bool UseMoneyTakeProfit { get => _useMoneyTakeProfit.Value; set => _useMoneyTakeProfit.Value = value; }
public decimal MoneyTakeProfit { get => _moneyTakeProfit.Value; set => _moneyTakeProfit.Value = value; }
public bool UsePercentTakeProfit { get => _usePercentTakeProfit.Value; set => _usePercentTakeProfit.Value = value; }
public decimal PercentTakeProfit { get => _percentTakeProfit.Value; set => _percentTakeProfit.Value = value; }
public bool EnableMoneyTrailing { get => _enableMoneyTrailing.Value; set => _enableMoneyTrailing.Value = value; }
public decimal MoneyTrailTarget { get => _moneyTrailTarget.Value; set => _moneyTrailTarget.Value = value; }
public decimal MoneyTrailStop { get => _moneyTrailStop.Value; set => _moneyTrailStop.Value = value; }
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
public bool UseBreakEven { get => _useBreakEven.Value; set => _useBreakEven.Value = value; }
public int BreakEvenTriggerPips { get => _breakEvenTriggerPips.Value; set => _breakEvenTriggerPips.Value = value; }
public int BreakEvenOffsetPips { get => _breakEvenOffsetPips.Value; set => _breakEvenOffsetPips.Value = value; }
public int FastMaPeriod { get => _fastMaPeriod.Value; set => _fastMaPeriod.Value = value; }
public int SlowMaPeriod { get => _slowMaPeriod.Value; set => _slowMaPeriod.Value = value; }
public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }
public decimal MomentumBuyThreshold { get => _momentumBuyThreshold.Value; set => _momentumBuyThreshold.Value = value; }
public decimal MomentumSellThreshold { get => _momentumSellThreshold.Value; set => _momentumSellThreshold.Value = value; }
public int MacdFastLength { get => _macdFastLength.Value; set => _macdFastLength.Value = value; }
public int MacdSlowLength { get => _macdSlowLength.Value; set => _macdSlowLength.Value = value; }
public int MacdSignalLength { get => _macdSignalLength.Value; set => _macdSignalLength.Value = value; }
public bool UseEquityStop { get => _useEquityStop.Value; set => _useEquityStop.Value = value; }
public decimal EquityRiskPercent { get => _equityRiskPercent.Value; set => _equityRiskPercent.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DataType MomentumCandleType { get => _momentumCandleType.Value; set => _momentumCandleType.Value = value; }
public DataType MacdCandleType { get => _macdCandleType.Value; set => _macdCandleType.Value = value; }
public CryptoAnalysisStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
_useMoneyTakeProfit = Param(nameof(UseMoneyTakeProfit), false);
_moneyTakeProfit = Param(nameof(MoneyTakeProfit), 100m).SetNotNegative();
_usePercentTakeProfit = Param(nameof(UsePercentTakeProfit), false);
_percentTakeProfit = Param(nameof(PercentTakeProfit), 1m).SetNotNegative();
_enableMoneyTrailing = Param(nameof(EnableMoneyTrailing), false);
_moneyTrailTarget = Param(nameof(MoneyTrailTarget), 50m).SetNotNegative();
_moneyTrailStop = Param(nameof(MoneyTrailStop), 20m).SetNotNegative();
_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
_takeProfitPips = Param(nameof(TakeProfitPips), 100).SetNotNegative();
_trailingStopPips = Param(nameof(TrailingStopPips), 30).SetNotNegative();
_useBreakEven = Param(nameof(UseBreakEven), true);
_breakEvenTriggerPips = Param(nameof(BreakEvenTriggerPips), 30).SetNotNegative();
_breakEvenOffsetPips = Param(nameof(BreakEvenOffsetPips), 2).SetNotNegative();
_fastMaPeriod = Param(nameof(FastMaPeriod), 6).SetGreaterThanZero();
_slowMaPeriod = Param(nameof(SlowMaPeriod), 85).SetGreaterThanZero();
_momentumPeriod = Param(nameof(MomentumPeriod), 14).SetGreaterThanZero();
_momentumBuyThreshold = Param(nameof(MomentumBuyThreshold), 0.3m).SetNotNegative();
_momentumSellThreshold = Param(nameof(MomentumSellThreshold), 0.3m).SetNotNegative();
_macdFastLength = Param(nameof(MacdFastLength), 12).SetGreaterThanZero();
_macdSlowLength = Param(nameof(MacdSlowLength), 26).SetGreaterThanZero();
_macdSignalLength = Param(nameof(MacdSignalLength), 9).SetGreaterThanZero();
_useEquityStop = Param(nameof(UseEquityStop), false);
_equityRiskPercent = Param(nameof(EquityRiskPercent), 10m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
_momentumCandleType = Param(nameof(MomentumCandleType), TimeSpan.FromHours(1).TimeFrame());
_macdCandleType = Param(nameof(MacdCandleType), TimeSpan.FromHours(4).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, MomentumCandleType), (Security, MacdCandleType)];
protected override void OnReseted()
{
base.OnReseted();
_rsi.Reset();
_primary.Clear();
_momentumCloses.Clear();
_momentumDeviations.Clear();
_macdFast = null;
_macdSlow = null;
_macdSignal = null;
_macdReady = false;
_macdCount = 0;
ResetTradeState();
_initialEquity = 0m;
_peakEquity = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_initialEquity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
_peakEquity = _initialEquity;
SubscribeCandles(MomentumCandleType).Bind(ProcessMomentum).Start();
SubscribeCandles(MacdCandleType).Bind(ProcessMacd).Start();
SubscribeCandles(CandleType).Bind(ProcessPrimary).Start();
}
private void ProcessMomentum(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_momentumCloses.Add(candle.ClosePrice);
var keep = MomentumPeriod + 5;
if (_momentumCloses.Count > keep)
_momentumCloses.RemoveRange(0, _momentumCloses.Count - keep);
if (_momentumCloses.Count <= MomentumPeriod)
return;
var previous = _momentumCloses[^(MomentumPeriod + 1)];
if (previous == 0m)
return;
var momentum = candle.ClosePrice / previous * 100m;
_momentumDeviations.Enqueue(Math.Abs(momentum - 100m));
while (_momentumDeviations.Count > 3)
_momentumDeviations.Dequeue();
}
private void ProcessMacd(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_macdCount++;
_macdFast = Ema(_macdFast, candle.ClosePrice, MacdFastLength);
_macdSlow = Ema(_macdSlow, candle.ClosePrice, MacdSlowLength);
var main = _macdFast.Value - _macdSlow.Value;
_macdSignal = Ema(_macdSignal, main, MacdSignalLength);
_macdReady = _macdCount >= MacdSlowLength + MacdSignalLength;
}
private void ProcessPrimary(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var required = Math.Max(Math.Max(FastMaPeriod, SlowMaPeriod), _bandLength + 1);
_primary.Add(new(candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice));
if (_primary.Count > required + 5)
_primary.RemoveRange(0, _primary.Count - required - 5);
var rsiValue = _rsi.Process(candle.ClosePrice, candle.ServerTime, true);
if (Position != 0 && ApplyRisk(candle))
return;
if (_primary.Count < required ||
!_rsi.IsFormed ||
_momentumDeviations.Count < 3 ||
!_macdReady ||
_macdSignal is null ||
_macdFast is null ||
_macdSlow is null)
return;
var previous = _primary[^2];
var (upper, lower) = BollingerAtPrevious();
var fast = Lwma(FastMaPeriod);
var slow = Lwma(SlowMaPeriod);
var rsi = rsiValue.ToDecimal();
var momentum = _momentumDeviations.Max();
var macdMain = _macdFast.Value - _macdSlow.Value;
var macdSignal = _macdSignal.Value;
var longSignal =
previous.Low <= lower &&
fast < slow &&
rsi > 50m &&
momentum > MomentumBuyThreshold &&
macdMain > macdSignal;
var shortSignal =
previous.High >= upper &&
fast < slow &&
rsi < 50m &&
momentum > MomentumSellThreshold &&
macdMain < macdSignal;
if (longSignal && Position <= 0m)
Enter(Sides.Buy, candle.ClosePrice);
else if (shortSignal && Position >= 0m)
Enter(Sides.Sell, candle.ClosePrice);
}
private void Enter(Sides side, decimal price)
{
var volume = OrderVolume + Math.Abs(Position);
if (side == Sides.Buy)
BuyMarket(volume);
else
SellMarket(volume);
_entryPrice = price;
var pip = GetPipSize();
_stopPrice = StopLossPips > 0
? side == Sides.Buy ? price - StopLossPips * pip : price + StopLossPips * pip
: null;
_takePrice = TakeProfitPips > 0
? side == Sides.Buy ? price + TakeProfitPips * pip : price - TakeProfitPips * pip
: null;
_bestPrice = price;
_moneyTrailPeak = null;
}
private bool ApplyRisk(ICandleMessage candle)
{
var floating = FloatingPnL(candle.ClosePrice);
var equity = (Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? _initialEquity) + floating;
_peakEquity = Math.Max(_peakEquity, equity);
// The levels are checked as they stood before this candle; moving them afterwards keeps
// a move made from this candle's high or low from being hit by the same candle.
if (IsLevelHit(candle))
return Flatten();
if (UseMoneyTakeProfit && floating >= MoneyTakeProfit)
return Flatten();
if (UsePercentTakeProfit && _initialEquity > 0m && floating >= _initialEquity * PercentTakeProfit / 100m)
return Flatten();
if (EnableMoneyTrailing)
{
// Once armed, the peak is tracked and the giveback checked even below the target.
if (_moneyTrailPeak is decimal peak)
_moneyTrailPeak = Math.Max(peak, floating);
else if (floating > MoneyTrailTarget)
_moneyTrailPeak = floating;
if (_moneyTrailPeak is decimal armedPeak && armedPeak - floating >= MoneyTrailStop)
return Flatten();
}
if (UseEquityStop && _peakEquity > 0m && (_peakEquity - equity) / _peakEquity * 100m > EquityRiskPercent)
return Flatten();
MoveLevels(candle);
return false;
}
private bool IsLevelHit(ICandleMessage candle)
{
if (Position > 0m)
{
return (_stopPrice is decimal longStop && candle.LowPrice <= longStop) ||
(_takePrice is decimal longTake && candle.HighPrice >= longTake);
}
return (_stopPrice is decimal shortStop && candle.HighPrice >= shortStop) ||
(_takePrice is decimal shortTake && candle.LowPrice <= shortTake);
}
private void MoveLevels(ICandleMessage candle)
{
var pip = GetPipSize();
if (Position > 0m)
{
if (UseBreakEven && candle.HighPrice - _entryPrice >= BreakEvenTriggerPips * pip)
RaiseStop(_entryPrice + BreakEvenOffsetPips * pip);
// The stop trails only a new high beyond the best price since the entry.
if (candle.HighPrice > _bestPrice)
{
_bestPrice = candle.HighPrice;
if (TrailingStopPips > 0)
RaiseStop(_bestPrice - TrailingStopPips * pip);
}
}
else
{
if (UseBreakEven && _entryPrice - candle.LowPrice >= BreakEvenTriggerPips * pip)
LowerStop(_entryPrice - BreakEvenOffsetPips * pip);
if (candle.LowPrice < _bestPrice)
{
_bestPrice = candle.LowPrice;
if (TrailingStopPips > 0)
LowerStop(_bestPrice + TrailingStopPips * pip);
}
}
}
private void RaiseStop(decimal level)
{
if (_stopPrice is not decimal stop || level > stop)
_stopPrice = level;
}
private void LowerStop(decimal level)
{
if (_stopPrice is not decimal stop || level < stop)
_stopPrice = level;
}
private bool Flatten()
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else if (Position < 0m)
BuyMarket(Math.Abs(Position));
else
return false;
ResetTradeState();
return true;
}
private decimal FloatingPnL(decimal price)
{
if (Position == 0m || _entryPrice == 0m)
return 0m;
var direction = Position > 0m ? 1m : -1m;
return (price - _entryPrice) * direction * Math.Abs(Position) * (Security?.Multiplier ?? 1m);
}
private (decimal Upper, decimal Lower) BollingerAtPrevious()
{
var end = _primary.Count - 1;
var total = 0m;
for (var i = end - _bandLength; i < end; i++)
total += _primary[i].Close;
var mean = total / _bandLength;
var squares = 0m;
for (var i = end - _bandLength; i < end; i++)
{
var deviation = _primary[i].Close - mean;
squares += deviation * deviation;
}
var std = (decimal)Math.Sqrt((double)(squares / _bandLength));
return (mean + _bandWidth * std, mean - _bandWidth * std);
}
private decimal Lwma(int period)
{
var start = _primary.Count - period;
var total = 0m;
var weights = 0m;
for (var i = 0; i < period; i++)
{
var weight = i + 1m;
total += _primary[start + i].Typical * weight;
weights += weight;
}
return total / weights;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m) return 0.0001m;
return step is 0.00001m or 0.001m ? step * 10m : step;
}
private static decimal Ema(decimal? previous, decimal value, int period)
{
if (previous is null) return value;
var alpha = 2m / (period + 1m);
return previous.Value + alpha * (value - previous.Value);
}
private void ResetTradeState()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_bestPrice = 0m;
_moneyTrailPeak = null;
}
private readonly record struct Bar(decimal Open, decimal High, decimal Low, decimal Close)
{
public decimal Typical => (High + Low + Close) / 3m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class crypto_analysis_strategy(Strategy):
RSI_LENGTH = 14
BAND_LENGTH = 20
BAND_WIDTH = 2
def __init__(self):
super(crypto_analysis_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", Decimal(0.1)).SetGreaterThanZero()
self._use_money_tp = self.Param("UseMoneyTakeProfit", False)
self._money_tp = self.Param("MoneyTakeProfit", Decimal(100)).SetNotNegative()
self._use_percent_tp = self.Param("UsePercentTakeProfit", False)
self._percent_tp = self.Param("PercentTakeProfit", Decimal(1)).SetNotNegative()
self._enable_money_trailing = self.Param("EnableMoneyTrailing", False)
self._money_trail_target = self.Param("MoneyTrailTarget", Decimal(50)).SetNotNegative()
self._money_trail_stop = self.Param("MoneyTrailStop", Decimal(20)).SetNotNegative()
self._stop_loss = self.Param("StopLossPips", 50).SetNotNegative()
self._take_profit = self.Param("TakeProfitPips", 100).SetNotNegative()
self._trailing_stop = self.Param("TrailingStopPips", 30).SetNotNegative()
self._use_break_even = self.Param("UseBreakEven", True)
self._break_even_trigger = self.Param("BreakEvenTriggerPips", 30).SetNotNegative()
self._break_even_offset = self.Param("BreakEvenOffsetPips", 2).SetNotNegative()
self._fast_ma = self.Param("FastMaPeriod", 6).SetGreaterThanZero()
self._slow_ma = self.Param("SlowMaPeriod", 85).SetGreaterThanZero()
self._momentum_period = self.Param("MomentumPeriod", 14).SetGreaterThanZero()
self._momentum_buy = self.Param("MomentumBuyThreshold", Decimal(0.3)).SetNotNegative()
self._momentum_sell = self.Param("MomentumSellThreshold", Decimal(0.3)).SetNotNegative()
self._macd_fast_len = self.Param("MacdFastLength", 12).SetGreaterThanZero()
self._macd_slow_len = self.Param("MacdSlowLength", 26).SetGreaterThanZero()
self._macd_signal_len = self.Param("MacdSignalLength", 9).SetGreaterThanZero()
self._use_equity_stop = self.Param("UseEquityStop", False)
self._equity_risk = self.Param("EquityRiskPercent", Decimal(10)).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._momentum_type = self.Param("MomentumCandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._macd_type = self.Param("MacdCandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RSI_LENGTH
self._primary = []
self._momentum_closes = []
self._momentum_deviations = []
self._macd_fast = None
self._macd_slow = None
self._macd_signal = None
self._macd_ready = False
self._macd_count = 0
self._entry_price = Decimal.Zero
self._stop_price = None
self._take_price = None
self._best_price = Decimal.Zero
self._initial_equity = Decimal.Zero
self._peak_equity = Decimal.Zero
self._money_trail_peak = None
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value), (self.Security, self._momentum_type.Value), (self.Security, self._macd_type.Value)]
def OnReseted(self):
super(crypto_analysis_strategy, self).OnReseted()
self._rsi.Reset()
self._primary = []
self._momentum_closes = []
self._momentum_deviations = []
self._macd_fast = self._macd_slow = self._macd_signal = None
self._macd_ready = False
self._macd_count = 0
self._reset_trade()
self._initial_equity = Decimal.Zero
self._peak_equity = Decimal.Zero
def OnStarted2(self, time):
super(crypto_analysis_strategy, self).OnStarted2(time)
self._initial_equity = self._portfolio_value(Decimal.Zero)
self._peak_equity = self._initial_equity
self.SubscribeCandles(self._momentum_type.Value).Bind(self._process_momentum).Start()
self.SubscribeCandles(self._macd_type.Value).Bind(self._process_macd).Start()
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_primary).Start()
def _process_momentum(self, candle):
if candle.State != CandleStates.Finished:
return
period = self._momentum_period.Value
self._momentum_closes.append(candle.ClosePrice)
keep = period + 5
if len(self._momentum_closes) > keep:
del self._momentum_closes[:len(self._momentum_closes) - keep]
if len(self._momentum_closes) <= period:
return
previous = self._momentum_closes[-1 - period]
if previous == Decimal.Zero:
return
momentum = candle.ClosePrice / previous * Decimal(100)
self._momentum_deviations.append(Math.Abs(momentum - Decimal(100)))
if len(self._momentum_deviations) > 3:
del self._momentum_deviations[:-3]
def _process_macd(self, candle):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
self._macd_count += 1
self._macd_fast = self._ema(self._macd_fast, close, self._macd_fast_len.Value)
self._macd_slow = self._ema(self._macd_slow, close, self._macd_slow_len.Value)
main = self._macd_fast - self._macd_slow
self._macd_signal = self._ema(self._macd_signal, main, self._macd_signal_len.Value)
self._macd_ready = self._macd_count >= self._macd_slow_len.Value + self._macd_signal_len.Value
def _process_primary(self, candle):
if candle.State != CandleStates.Finished:
return
required = max(max(self._fast_ma.Value, self._slow_ma.Value), self.BAND_LENGTH + 1)
self._primary.append((candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice))
if len(self._primary) > required + 5:
del self._primary[:len(self._primary) - required - 5]
rsi_value = process_value(self._rsi, candle.ClosePrice, candle.ServerTime, True)
if self.Position != 0 and self._apply_risk(candle):
return
if (len(self._primary) < required or not self._rsi.IsFormed or len(self._momentum_deviations) < 3 or
not self._macd_ready or self._macd_signal is None or self._macd_fast is None or self._macd_slow is None):
return
previous = self._primary[-2]
upper, lower = self._bollinger_previous()
fast = self._lwma(self._fast_ma.Value)
slow = self._lwma(self._slow_ma.Value)
rsi = to_decimal(rsi_value)
momentum = max(self._momentum_deviations)
macd_main = self._macd_fast - self._macd_slow
macd_signal = self._macd_signal
fifty = Decimal(50)
long_signal = (previous[2] <= lower and fast < slow and rsi > fifty and
momentum > self._momentum_buy.Value and macd_main > macd_signal)
short_signal = (previous[1] >= upper and fast < slow and rsi < fifty and
momentum > self._momentum_sell.Value and macd_main < macd_signal)
if long_signal and self.Position <= 0:
self._enter(Sides.Buy, candle.ClosePrice)
elif short_signal and self.Position >= 0:
self._enter(Sides.Sell, candle.ClosePrice)
def _enter(self, side, price):
volume = self._order_volume.Value + Math.Abs(self.Position)
if side == Sides.Buy:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
self._entry_price = price
pip = self._pip_size()
if self._stop_loss.Value > 0:
distance = Decimal(self._stop_loss.Value) * pip
self._stop_price = price - distance if side == Sides.Buy else price + distance
else:
self._stop_price = None
if self._take_profit.Value > 0:
distance = Decimal(self._take_profit.Value) * pip
self._take_price = price + distance if side == Sides.Buy else price - distance
else:
self._take_price = None
self._best_price = price
self._money_trail_peak = None
def _apply_risk(self, candle):
floating = self._floating_pnl(candle.ClosePrice)
equity = self._portfolio_value(self._initial_equity) + floating
if equity > self._peak_equity:
self._peak_equity = equity
# The levels are checked as they stood before this candle; moving them afterwards keeps
# a move made from this candle's high or low from being hit by the same candle.
if self._is_level_hit(candle):
return self._flatten()
if self._use_money_tp.Value and floating >= self._money_tp.Value:
return self._flatten()
if (self._use_percent_tp.Value and self._initial_equity > 0 and
floating >= self._initial_equity * self._percent_tp.Value / Decimal(100)):
return self._flatten()
if self._enable_money_trailing.Value:
# Once armed, the peak is tracked and the giveback checked even below the target.
if self._money_trail_peak is not None:
if floating > self._money_trail_peak:
self._money_trail_peak = floating
elif floating > self._money_trail_target.Value:
self._money_trail_peak = floating
if self._money_trail_peak is not None and self._money_trail_peak - floating >= self._money_trail_stop.Value:
return self._flatten()
if (self._use_equity_stop.Value and self._peak_equity > 0 and
(self._peak_equity - equity) / self._peak_equity * Decimal(100) > self._equity_risk.Value):
return self._flatten()
self._move_levels(candle)
return False
def _is_level_hit(self, candle):
if self.Position > 0:
return ((self._stop_price is not None and candle.LowPrice <= self._stop_price) or
(self._take_price is not None and candle.HighPrice >= self._take_price))
return ((self._stop_price is not None and candle.HighPrice >= self._stop_price) or
(self._take_price is not None and candle.LowPrice <= self._take_price))
def _move_levels(self, candle):
pip = self._pip_size()
if self.Position > 0:
if (self._use_break_even.Value and
candle.HighPrice - self._entry_price >= Decimal(self._break_even_trigger.Value) * pip):
self._raise_stop(self._entry_price + Decimal(self._break_even_offset.Value) * pip)
# The stop trails only a new high beyond the best price since the entry.
if candle.HighPrice > self._best_price:
self._best_price = candle.HighPrice
if self._trailing_stop.Value > 0:
self._raise_stop(self._best_price - Decimal(self._trailing_stop.Value) * pip)
else:
if (self._use_break_even.Value and
self._entry_price - candle.LowPrice >= Decimal(self._break_even_trigger.Value) * pip):
self._lower_stop(self._entry_price - Decimal(self._break_even_offset.Value) * pip)
if candle.LowPrice < self._best_price:
self._best_price = candle.LowPrice
if self._trailing_stop.Value > 0:
self._lower_stop(self._best_price + Decimal(self._trailing_stop.Value) * pip)
def _raise_stop(self, level):
if self._stop_price is None or level > self._stop_price:
self._stop_price = level
def _lower_stop(self, level):
if self._stop_price is None or level < self._stop_price:
self._stop_price = level
def _flatten(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
else:
return False
self._reset_trade()
return True
def _floating_pnl(self, price):
if self.Position == 0 or self._entry_price == Decimal.Zero:
return Decimal.Zero
direction = Decimal(1) if self.Position > 0 else Decimal(-1)
multiplier = Decimal(1)
if self.Security is not None and self.Security.Multiplier is not None:
multiplier = self.Security.Multiplier
return (price - self._entry_price) * direction * Math.Abs(self.Position) * multiplier
def _portfolio_value(self, fallback):
portfolio = self.Portfolio
if portfolio is not None:
if portfolio.CurrentValue is not None:
return portfolio.CurrentValue
if portfolio.BeginValue is not None:
return portfolio.BeginValue
return fallback
def _bollinger_previous(self):
end = len(self._primary) - 1
length = Decimal(self.BAND_LENGTH)
total = Decimal.Zero
for i in range(end - self.BAND_LENGTH, end):
total += self._primary[i][3]
mean = total / length
squares = Decimal.Zero
for i in range(end - self.BAND_LENGTH, end):
deviation = self._primary[i][3] - mean
squares += deviation * deviation
std = Decimal(Math.Sqrt(Decimal.ToDouble(squares / length)))
width = Decimal(self.BAND_WIDTH)
return mean + width * std, mean - width * std
def _lwma(self, period):
start = len(self._primary) - period
total = Decimal.Zero
weights = Decimal.Zero
for i in range(period):
weight = Decimal(i + 1)
bar = self._primary[start + i]
typical = (bar[1] + bar[2] + bar[3]) / Decimal(3)
total += typical * weight
weights += weight
return total / weights
def _pip_size(self):
step = Decimal.Zero
if self.Security is not None and self.Security.PriceStep is not None:
step = self.Security.PriceStep
if step <= 0:
return Decimal(0.0001)
if step == Decimal(0.00001) or step == Decimal(0.001):
return step * Decimal(10)
return step
@staticmethod
def _ema(previous, value, period):
if previous is None:
return value
alpha = Decimal(2) / (Decimal(period) + Decimal(1))
return previous + alpha * (value - previous)
def _reset_trade(self):
self._entry_price = Decimal.Zero
self._stop_price = None
self._take_price = None
self._best_price = Decimal.Zero
self._money_trail_peak = None
def CreateClone(self):
return crypto_analysis_strategy()