Precision Trading: Golden Edge
Стратегия пересечения EMA с подтверждением направления Hull, импульса RSI и волатильности ATR/диапазона high-low. Историческое описание относилось к скальпингу золота; пример работает с выбранным инструментом и не доказывает результативность на золоте.
Сигналы и исполнение
Используются только завершённые свечи, по умолчанию пятиминутные. Все семь индикаторов должны сформироваться. Первая полностью прогретая свеча запоминает предыдущие EMA/HMA без входа.
- Лонг: предыдущая быстрая EMA ≤ предыдущей медленной EMA, текущая быстрая EMA > текущей медленной EMA, текущая HMA > предыдущей HMA и RSI >
RsiLongThreshold (55).
- Шорт: предыдущая быстрая EMA ≥ предыдущей медленной EMA, текущая быстрая EMA < текущей медленной EMA, текущая HMA < предыдущей HMA и RSI <
RsiShortThreshold (45).
- Для обоих направлений нужны положительные close и ATR,
100 * ATR / close >= MinAtrPercent (0.05) и Highest(high) - Lowest(low) >= ATR * RangeAtrMultiplier (3). Окно high/low включает текущую свечу.
Подтверждённое пересечение открывает Volume из нулевой позиции либо отправляет Volume + abs(Position) для полного разворота встречной позиции. Сигнал в сторону уже открытой позиции её сохраняет. Незавершённый ордер блокирует повторную отправку. Предыдущие EMA/HMA обновляются на каждой полностью прогретой свече: отклонённое пересечение не откладывается для последующего входа. Шестичасового cooldown нет. Отдельных стоп-лосса и тейк-профита нет; встречное подтверждённое пересечение разворачивает позицию.
Индикаторы и настройки по умолчанию
| Параметр |
Значение |
Смысл |
FastLength / SlowLength |
14 / 40 |
Штатные EMA закрытий; быстрая короче медленной |
HmaLength |
55 |
WMA(floor(sqrt(L)), 2 * WMA(floor(L/2), close) - WMA(L, close)); длина ≥ 2 |
RsiLength |
14 |
Штатный RSI с Wilder-сглаживанием положительных и отрицательных изменений close |
RsiLongThreshold / RsiShortThreshold |
55 / 45 |
Строгие пороги; 0 <= short < long <= 100 |
AtrLength |
14 |
Штатный Wilder ATR с учётом разрывов относительно предыдущего close |
RangeLength |
20 |
Максимальный high и минимальный low, включая текущую свечу |
MinAtrPercent |
0.05 |
Включительный минимум ATR в процентах текущего close; неотрицательный |
RangeAtrMultiplier |
3 |
Включительный минимум диапазона high-low в единицах ATR; положительный |
CandleType |
5 минут |
Завершённые свечи для всех индикаторов |
Веса WMA растут от 1 для старого наблюдения до длины окна для нового. EMA начинается со штатного среднего арифметического, затем использует 2 / (length + 1). ATR начинает со среднего по растущему окну, затем использует сглаживание Wilder. Периоды положительны; Hull дополнительно прогревает заключительную WMA длины квадратного корня.
Поведенческие тесты C# и Python независимо рассчитывают EMA, HMA, RSI, ATR и скользящие экстремумы на имеющемся архиве BTC/TON. Проверяются каждый отправленный ордер, полные развороты, отклонённые подтверждения и сигналы ближе шести часов. Это проверка поведения, а не доходности или пригодности для реальной торговли.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Confirms EMA crosses with Hull slope, RSI and ATR/price-range volatility,
/// then opens or fully reverses on finished candles.
/// </summary>
public class PrecisionTradingGoldenEdgeStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _hmaLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<decimal> _rsiLongThreshold;
private readonly StrategyParam<decimal> _rsiShortThreshold;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _rangeLength;
private readonly StrategyParam<decimal> _minAtrPercent;
private readonly StrategyParam<decimal> _rangeAtrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private bool _isInitialized;
private decimal _previousFast;
private decimal _previousSlow;
private decimal _previousHma;
private Order _pendingOrder;
/// <summary>Initializes EMA14/40, HMA55, RSI14 at 55/45, ATR14 and twenty-bar range confirmation.</summary>
public PrecisionTradingGoldenEdgeStrategy()
{
_fastLength = Param(nameof(FastLength), 14).SetGreaterThanZero()
.SetDisplay("Fast Length", "Fast close EMA period", "Trend");
_slowLength = Param(nameof(SlowLength), 40).SetGreaterThanZero()
.SetDisplay("Slow Length", "Slow close EMA period", "Trend");
_hmaLength = Param(nameof(HmaLength), 55).SetRange(2, int.MaxValue)
.SetDisplay("HMA Length", "Hull close average with integer half and square-root lengths", "Trend");
_rsiLength = Param(nameof(RsiLength), 14).SetGreaterThanZero()
.SetDisplay("RSI Length", "Close momentum period", "Momentum");
_rsiLongThreshold = Param(nameof(RsiLongThreshold), 55m).SetRange(0m, 100m)
.SetDisplay("Long RSI Threshold", "Strict lower bound for long confirmation", "Momentum");
_rsiShortThreshold = Param(nameof(RsiShortThreshold), 45m).SetRange(0m, 100m)
.SetDisplay("Short RSI Threshold", "Strict upper bound for short confirmation", "Momentum");
_atrLength = Param(nameof(AtrLength), 14).SetGreaterThanZero()
.SetDisplay("ATR Length", "Wilder true-range period", "Volatility");
_rangeLength = Param(nameof(RangeLength), 20).SetGreaterThanZero()
.SetDisplay("Range Length", "Current-inclusive high/low window", "Volatility");
_minAtrPercent = Param(nameof(MinAtrPercent), 0.05m).SetRange(0m, decimal.MaxValue)
.SetDisplay("Minimum ATR Percent", "Inclusive minimum ATR as percent of current close", "Volatility");
_rangeAtrMultiplier = Param(nameof(RangeAtrMultiplier), 3m).SetGreaterThanZero()
.SetDisplay("Range ATR Multiplier", "Inclusive minimum highest-lowest span as an ATR multiple", "Volatility");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished candles supplying all confirmations", "General");
OrderRegistering += order => _pendingOrder = order;
}
/// <summary>Fast close EMA length, strictly less than SlowLength.</summary>
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
/// <summary>Slow close EMA length.</summary>
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
/// <summary>Hull close average length, at least two.</summary>
public int HmaLength { get => _hmaLength.Value; set => _hmaLength.Value = value; }
/// <summary>Native close RSI length.</summary>
public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
/// <summary>Strict long RSI threshold.</summary>
public decimal RsiLongThreshold { get => _rsiLongThreshold.Value; set => _rsiLongThreshold.Value = value; }
/// <summary>Strict short RSI threshold, below RsiLongThreshold.</summary>
public decimal RsiShortThreshold { get => _rsiShortThreshold.Value; set => _rsiShortThreshold.Value = value; }
/// <summary>Wilder true-range length.</summary>
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <summary>Current-inclusive highest-high/lowest-low window length.</summary>
public int RangeLength { get => _rangeLength.Value; set => _rangeLength.Value = value; }
/// <summary>Inclusive minimum ATR/close percentage.</summary>
public decimal MinAtrPercent { get => _minAtrPercent.Value; set => _minAtrPercent.Value = value; }
/// <summary>Inclusive minimum high-low window span as an ATR multiple.</summary>
public decimal RangeAtrMultiplier { get => _rangeAtrMultiplier.Value; set => _rangeAtrMultiplier.Value = value; }
/// <summary>Finished candle type; five minutes by default.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
private void ResetState()
{
_isInitialized = false;
_previousFast = _previousSlow = _previousHma = 0m;
_pendingOrder = null;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (FastLength <= 0 || SlowLength <= FastLength || HmaLength < 2 || RsiLength <= 0 || AtrLength <= 0
|| RangeLength <= 0 || MinAtrPercent < 0m || RangeAtrMultiplier <= 0m
|| RsiShortThreshold < 0m || RsiLongThreshold > 100m || RsiShortThreshold >= RsiLongThreshold)
throw new ArgumentException("EMA lengths must be ordered, HMA length at least two, other periods/multiplier positive, and RSI thresholds ordered within [0, 100].");
ResetState();
var fast = new ExponentialMovingAverage { Length = FastLength };
var slow = new ExponentialMovingAverage { Length = SlowLength };
var hma = new HullMovingAverage { Length = HmaLength };
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var atr = new AverageTrueRange { Length = AtrLength };
var highest = new Highest { Length = RangeLength };
var lowest = new Lowest { Length = RangeLength };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx([fast, slow, hma, rsi, atr, highest, lowest], ProcessCandle, allowEmpty: true).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, fast);
DrawIndicator(area, slow);
DrawIndicator(area, hma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
{
if (candle.State != CandleStates.Finished || values.Any(value => value.IsEmpty || !value.IsFormed))
return;
var fast = values[0].ToDecimal();
var slow = values[1].ToDecimal();
var hma = values[2].ToDecimal();
var rsi = values[3].ToDecimal();
var atr = values[4].ToDecimal();
var span = values[5].ToDecimal() - values[6].ToDecimal();
var volatilityPass = candle.ClosePrice > 0m && atr > 0m && 100m * atr / candle.ClosePrice >= MinAtrPercent
&& span >= atr * RangeAtrMultiplier;
if (_isInitialized && volatilityPass && IsFormedAndOnlineAndAllowTrading()
&& (_pendingOrder is null || _pendingOrder.State is OrderStates.Done or OrderStates.Failed))
{
Sides? side = null;
if (_previousFast <= _previousSlow && fast > slow && hma > _previousHma && rsi > RsiLongThreshold && Position <= 0m)
side = Sides.Buy;
else if (_previousFast >= _previousSlow && fast < slow && hma < _previousHma && rsi < RsiShortThreshold && Position >= 0m)
side = Sides.Sell;
if (side is Sides direction)
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = direction,
Volume = Volume + Math.Abs(Position),
Comment = "Golden Edge confirmed cross",
});
}
_isInitialized = true;
_previousFast = fast;
_previousSlow = slow;
_previousHma = hma;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Array, ArgumentException, Decimal, Int32, TimeSpan
from StockSharp.Messages import CandleStates, DataType, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order
from StockSharp.Algo.Indicators import AverageTrueRange, ExponentialMovingAverage, Highest, HullMovingAverage, IIndicator, Lowest, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class precision_trading_golden_edge_strategy(Strategy):
def __init__(self):
super(precision_trading_golden_edge_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 14).SetGreaterThanZero() \
.SetDisplay("Fast Length", "Fast close EMA period", "Trend")
self._slow_length = self.Param("SlowLength", 40).SetGreaterThanZero() \
.SetDisplay("Slow Length", "Slow close EMA period", "Trend")
self._hma_length = self.Param("HmaLength", 55).SetRange(2, Int32.MaxValue) \
.SetDisplay("HMA Length", "Hull close average with integer half and square-root lengths", "Trend")
self._rsi_length = self.Param("RsiLength", 14).SetGreaterThanZero() \
.SetDisplay("RSI Length", "Close momentum period", "Momentum")
self._rsi_long_threshold = self.Param("RsiLongThreshold", Decimal(55)).SetRange(Decimal.Zero, Decimal(100)) \
.SetDisplay("Long RSI Threshold", "Strict lower bound for long confirmation", "Momentum")
self._rsi_short_threshold = self.Param("RsiShortThreshold", Decimal(45)).SetRange(Decimal.Zero, Decimal(100)) \
.SetDisplay("Short RSI Threshold", "Strict upper bound for short confirmation", "Momentum")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero() \
.SetDisplay("ATR Length", "Wilder true-range period", "Volatility")
self._range_length = self.Param("RangeLength", 20).SetGreaterThanZero() \
.SetDisplay("Range Length", "Current-inclusive high/low window", "Volatility")
self._min_atr_percent = self.Param("MinAtrPercent", Decimal.Parse("0.05")).SetRange(Decimal.Zero, Decimal.MaxValue) \
.SetDisplay("Minimum ATR Percent", "Inclusive minimum ATR as percent of current close", "Volatility")
self._range_atr_multiplier = self.Param("RangeAtrMultiplier", Decimal(3)).SetGreaterThanZero() \
.SetDisplay("Range ATR Multiplier", "Inclusive minimum highest-lowest span as an ATR multiple", "Volatility")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying all confirmations", "General")
self._reset_state()
self.OrderRegistering += self._observe_order
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._is_initialized = False
self._previous_fast = self._previous_slow = self._previous_hma = Decimal.Zero
self._pending_order = None
def OnReseted(self):
super(precision_trading_golden_edge_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(precision_trading_golden_edge_strategy, self).OnStarted2(time)
if self._fast_length.Value <= 0 or self._slow_length.Value <= self._fast_length.Value \
or self._hma_length.Value < 2 or self._rsi_length.Value <= 0 or self._atr_length.Value <= 0 \
or self._range_length.Value <= 0 or self._min_atr_percent.Value < 0 or self._range_atr_multiplier.Value <= 0 \
or self._rsi_short_threshold.Value < 0 or self._rsi_long_threshold.Value > 100 \
or self._rsi_short_threshold.Value >= self._rsi_long_threshold.Value:
raise ArgumentException("EMA lengths must be ordered, HMA length at least two, other periods/multiplier positive, and RSI thresholds ordered within [0, 100].")
self._reset_state()
fast = ExponentialMovingAverage()
fast.Length = self._fast_length.Value
slow = ExponentialMovingAverage()
slow.Length = self._slow_length.Value
hma = HullMovingAverage()
hma.Length = self._hma_length.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
highest = Highest()
highest.Length = self._range_length.Value
lowest = Lowest()
lowest.Length = self._range_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(Array[IIndicator]([fast, slow, hma, rsi, atr, highest, lowest]), self.OnProcess, True).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawIndicator(area, hma)
self.DrawOwnTrades(area)
def OnProcess(self, candle, values):
if candle.State != CandleStates.Finished or any(value.IsEmpty or not value.IsFormed for value in values):
return
fast, slow, hma, rsi, atr, highest, lowest = [value.GetValue[Decimal](None) for value in values]
volatility_pass = candle.ClosePrice > 0 and atr > 0 \
and Decimal(100) * atr / candle.ClosePrice >= self._min_atr_percent.Value \
and highest - lowest >= atr * self._range_atr_multiplier.Value
pending = self._pending_order
if self._is_initialized and volatility_pass and self.IsFormedAndOnlineAndAllowTrading() \
and (pending is None or pending.State == OrderStates.Done or pending.State == OrderStates.Failed):
side = None
if self._previous_fast <= self._previous_slow and fast > slow and hma > self._previous_hma \
and rsi > self._rsi_long_threshold.Value and self.Position <= 0:
side = Sides.Buy
elif self._previous_fast >= self._previous_slow and fast < slow and hma < self._previous_hma \
and rsi < self._rsi_short_threshold.Value and self.Position >= 0:
side = Sides.Sell
if side is not None:
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume + abs(self.Position)
order.Comment = "Golden Edge confirmed cross"
self.RegisterOrder(order)
self._is_initialized = True
self._previous_fast = fast
self._previous_slow = slow
self._previous_hma = hma
def CreateClone(self):
return precision_trading_golden_edge_strategy()