Anomalous Holonomy Field Theory
Технический сигнал из шести компонентов: фиксированные EMA20/EMA50, RSI14 Уайлдера, MACD(12,26,9), ATR Уайлдера и нормированный на волатильность ROC. C — обычное закрытие; A=ATR; clip(x,k)=max(-k,min(k,x)). Все окна включают текущую завершённую свечу.
Правила торговли
T: +1,5 выше обеих EMA, +1 выше только EMA20, -1,5 ниже обеих, -1 ниже только EMA20, иначе ноль. Свечной VWAP: sum(((High+Low+Close)/3)*Volume)/sum(Volume) за VwapPeriod свечей. V=clip((C-VWAP)/A,2). Для B=T+V RSI добавляет R=+1,5 ниже 30 или +0,5 ниже 40 только при B>0; при B<0 добавляет -1,5 выше 70 или -0,5 выше 60; иначе R=0.
M=clip((MACD-EMA9(MACD))/A,1). Q=clip((C-C[RocPeriod свечей назад])/A,1) — ROC, делённый на ATR% с одинаковой базовой ценой прошлого периода. Итог: S=T+V+R+M+Q. Из нулевой позиции покупается Volume при S>=SignalThreshold, продаётся при S<=-SignalThreshold. Без прогрева, при неположительном ATR или нулевом объёме вход запрещён. Между индексами последовательных входов должно пройти строго больше CooldownBars свечей.
Перед каждым входом фиксируется расстояние ATR*AtrStopMultiplier. Нативный локальный стоп рыночной заявкой привязывает его к фактическому исполнению и реагирует на bid/ask между свечами. По умолчанию стоп равен 2 ATR. Открытая позиция выходит через защиту; противоположный сигнал её не наращивает и не разворачивает. Пауза между входами не задерживает защиту. Тейк-профита нет.
Входы разрешены только по завершённым свечам после прогрева индикаторов. Открытая позиция сопровождается до выхода; незавершённая рыночная заявка блокирует повторную отправку.
Параметры
| Parameter | Default |
|---|---|
SignalThreshold |
2 |
CooldownBars |
50 |
AtrLength |
14 |
AtrStopMultiplier |
2 |
RocPeriod |
10 |
VwapPeriod |
20 |
CandleType |
5m |
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>EMA, RSI, MACD, volatility-normalized ROC and bar VWAP composite with a frozen ATR stop.</summary>
public class AnomalousHolonomyFieldTheoryStrategy : Strategy
{
private readonly StrategyParam<decimal> _signalThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrStopMultiplier;
private readonly StrategyParam<int> _rocPeriod;
private readonly StrategyParam<int> _vwapPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _closes = new();
private readonly Queue<(decimal price, decimal volume)> _vwapBars = new();
private Order _pending;
private int _bar;
private int? _lastEntry;
/// <summary>Initializes the composite and risk parameters.</summary>
public AnomalousHolonomyFieldTheoryStrategy()
{
_signalThreshold = Param(nameof(SignalThreshold), 2m).SetGreaterThanZero()
.SetDisplay("SignalThreshold", "Absolute composite threshold", "Parameters");
_cooldownBars = Param(nameof(CooldownBars), 50).SetNotNegative()
.SetDisplay("CooldownBars", "Minimum finished bars between entries", "Parameters");
_atrLength = Param(nameof(AtrLength), 14).SetGreaterThanZero()
.SetDisplay("AtrLength", "Wilder ATR window", "Parameters");
_atrStopMultiplier = Param(nameof(AtrStopMultiplier), 2m).SetGreaterThanZero()
.SetDisplay("AtrStopMultiplier", "Entry-frozen ATR stop multiple", "Parameters");
_rocPeriod = Param(nameof(RocPeriod), 10).SetGreaterThanZero()
.SetDisplay("RocPeriod", "Rate-of-change interval", "Parameters");
_vwapPeriod = Param(nameof(VwapPeriod), 20).SetGreaterThanZero()
.SetDisplay("VwapPeriod", "Volume-weighted typical-price window", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters");
}
/// <summary>Absolute composite threshold.</summary>
public decimal SignalThreshold
{
get => _signalThreshold.Value;
set => _signalThreshold.Value = value;
}
/// <summary>Minimum finished bars between entries.</summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>Wilder ATR window.</summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>Entry-frozen ATR stop multiple.</summary>
public decimal AtrStopMultiplier
{
get => _atrStopMultiplier.Value;
set => _atrStopMultiplier.Value = value;
}
/// <summary>Rate-of-change interval.</summary>
public int RocPeriod
{
get => _rocPeriod.Value;
set => _rocPeriod.Value = value;
}
/// <summary>Volume-weighted typical-price window.</summary>
public int VwapPeriod
{
get => _vwapPeriod.Value;
set => _vwapPeriod.Value = value;
}
/// <summary>Finished signal candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_vwapBars.Clear();
_pending = null;
_bar = 0;
_lastEntry = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema20 = new ExponentialMovingAverage { Length = 20 };
var ema50 = new ExponentialMovingAverage { Length = 50 };
var rsi = new RelativeStrengthIndex { Length = 14 };
var macd = new MovingAverageConvergenceDivergenceSignal();
macd.Macd.ShortMa.Length = 12;
macd.Macd.LongMa.Length = 26;
macd.SignalMa.Length = 9;
var atr = new AverageTrueRange { Length = AtrLength };
var candles = SubscribeCandles(CandleType);
candles.BindEx([ema20, ema50, rsi, macd, atr], ProcessCandle).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawIndicator(area, ema20);
DrawIndicator(area, ema50);
DrawOwnTrades(area);
}
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Start();
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
{
if (candle.State != CandleStates.Finished)
return;
_bar++;
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > RocPeriod + 1)
_closes.Dequeue();
_vwapBars.Enqueue(((candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m, candle.TotalVolume));
if (_vwapBars.Count > VwapPeriod)
_vwapBars.Dequeue();
if (values.Any(v => !v.IsFormed) || _closes.Count <= RocPeriod || _vwapBars.Count < VwapPeriod
|| values[3] is not MovingAverageConvergenceDivergenceSignalValue macd
|| macd.Macd is not decimal line || macd.Signal is not decimal signal)
return;
var atr = values[4].GetValue<decimal>();
var volume = _vwapBars.Sum(b => b.volume);
if (atr <= 0m || volume <= 0m || _closes.Peek() <= 0m)
return;
var close = candle.ClosePrice;
var fast = values[0].GetValue<decimal>();
var slow = values[1].GetValue<decimal>();
var rsi = values[2].GetValue<decimal>();
var composite = close > fast ? close > slow ? 1.5m : 1m : close < fast ? close < slow ? -1.5m : -1m : 0m;
var vwap = _vwapBars.Sum(b => b.price * b.volume) / volume;
composite += Math.Clamp((close - vwap) / atr, -2m, 2m);
if (rsi < 30m && composite > 0m)
composite += 1.5m;
else if (rsi < 40m && composite > 0m)
composite += 0.5m;
else if (rsi > 70m && composite < 0m)
composite -= 1.5m;
else if (rsi > 60m && composite < 0m)
composite -= 0.5m;
composite += Math.Clamp((line - signal) / atr, -1m, 1m);
// ROC / ATR% uses the same lagged price denominator, so it simplifies to a price change / ATR.
composite += Math.Clamp((close - _closes.Peek()) / atr, -1m, 1m);
if (Position != 0m || _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed)
|| _lastEntry is int lastEntry && _bar - lastEntry <= CooldownBars || !IsFormedAndOnlineAndAllowTrading()
|| Math.Abs(composite) < SignalThreshold)
return;
StartProtection(new Unit(), new Unit(atr * AtrStopMultiplier, UnitTypes.Absolute), useMarketOrders: true, isLocalStop: true);
_lastEntry = _bar;
_pending = CreateOrder(composite > 0m ? Sides.Buy : Sides.Sell, 0m, Volume);
_pending.Type = OrderTypes.Market;
_pending.Comment = "0522Entry";
RegisterOrder(_pending);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Decimal, Math, TimeSpan, Convert, InvalidOperationException
from StockSharp.Algo.Indicators import BollingerBands, RelativeStrengthIndex, ExponentialMovingAverage, AverageTrueRange, DecimalIndicatorValue, MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderStates, OrderTypes, Sides, Unit, UnitTypes, ITickTradeMessage
class anomalous_holonomy_field_theory_strategy(Strategy):
"""Six-component signal with an entry-frozen ATR stop."""
def __init__(self):
super(anomalous_holonomy_field_theory_strategy, self).__init__()
self._signalThreshold = self.Param("SignalThreshold", Decimal(2)).SetGreaterThanZero() \
.SetDisplay("SignalThreshold", "Absolute composite threshold", "Parameters")
self._cooldownBars = self.Param("CooldownBars", 50).SetNotNegative() \
.SetDisplay("CooldownBars", "Minimum finished bars between entries", "Parameters")
self._atrLength = self.Param("AtrLength", 14).SetGreaterThanZero() \
.SetDisplay("AtrLength", "Wilder ATR window", "Parameters")
self._atrStopMultiplier = self.Param("AtrStopMultiplier", Decimal(2)).SetGreaterThanZero() \
.SetDisplay("AtrStopMultiplier", "Entry-frozen ATR stop multiple", "Parameters")
self._rocPeriod = self.Param("RocPeriod", 10).SetGreaterThanZero() \
.SetDisplay("RocPeriod", "Rate-of-change interval", "Parameters")
self._vwapPeriod = self.Param("VwapPeriod", 20).SetGreaterThanZero() \
.SetDisplay("VwapPeriod", "Volume-weighted typical-price window", "Parameters")
self._candleType = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters")
self._clear_state()
@property
def SignalThreshold(self):
return self._signalThreshold.Value
@SignalThreshold.setter
def SignalThreshold(self, value):
self._signalThreshold.Value = value
@property
def CooldownBars(self):
return self._cooldownBars.Value
@CooldownBars.setter
def CooldownBars(self, value):
self._cooldownBars.Value = value
@property
def AtrLength(self):
return self._atrLength.Value
@AtrLength.setter
def AtrLength(self, value):
self._atrLength.Value = value
@property
def AtrStopMultiplier(self):
return self._atrStopMultiplier.Value
@AtrStopMultiplier.setter
def AtrStopMultiplier(self, value):
self._atrStopMultiplier.Value = value
@property
def RocPeriod(self):
return self._rocPeriod.Value
@RocPeriod.setter
def RocPeriod(self, value):
self._rocPeriod.Value = value
@property
def VwapPeriod(self):
return self._vwapPeriod.Value
@VwapPeriod.setter
def VwapPeriod(self, value):
self._vwapPeriod.Value = value
@property
def CandleType(self):
return self._candleType.Value
@CandleType.setter
def CandleType(self, value):
self._candleType.Value = value
def _clear_state(self):
self._closes = []
self._vwap_bars = []
self._pending = None
self._bar = 0
self._last_entry = None
def OnReseted(self):
super(anomalous_holonomy_field_theory_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(anomalous_holonomy_field_theory_strategy, self).OnStarted2(time)
ema20 = ExponentialMovingAverage()
ema20.Length = 20
ema50 = ExponentialMovingAverage()
ema50.Length = 50
rsi = RelativeStrengthIndex()
rsi.Length = 14
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = 12
macd.Macd.LongMa.Length = 26
macd.SignalMa.Length = 9
atr = AverageTrueRange()
atr.Length = self.AtrLength
self._indicators = (ema20, ema50, rsi, macd, atr)
candles = self.SubscribeCandles(self.CandleType)
candles.BindEx(ema20, ema50, rsi, macd, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, ema20)
self.DrawIndicator(area, ema50)
self.DrawOwnTrades(area)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Start()
@staticmethod
def _clip(value, limit):
return max(-limit, min(limit, value))
def _process_candle(self, candle, fast_value, slow_value, rsi_value, macd_value, atr_value):
if candle.State != CandleStates.Finished:
return
self._bar += 1
self._closes.append(candle.ClosePrice)
if len(self._closes) > self.RocPeriod + 1:
self._closes.pop(0)
self._vwap_bars.append(((candle.HighPrice + candle.LowPrice + candle.ClosePrice) / Decimal(3), candle.TotalVolume))
if len(self._vwap_bars) > self.VwapPeriod:
self._vwap_bars.pop(0)
if not all(i.IsFormed for i in self._indicators) or len(self._closes) <= self.RocPeriod or len(self._vwap_bars) < self.VwapPeriod or macd_value.Macd is None or macd_value.Signal is None:
return
atr = atr_value.GetValue[Decimal](None)
volume = sum((b[1] for b in self._vwap_bars), Decimal(0))
if atr <= 0 or volume <= 0 or self._closes[0] <= 0:
return
close = candle.ClosePrice
fast = fast_value.GetValue[Decimal](None)
slow = slow_value.GetValue[Decimal](None)
rsi = rsi_value.GetValue[Decimal](None)
composite = Decimal(1.5) if close > fast and close > slow else Decimal(1) if close > fast else Decimal(-1.5) if close < fast and close < slow else Decimal(-1) if close < fast else Decimal(0)
vwap = sum((b[0] * b[1] for b in self._vwap_bars), Decimal(0)) / volume
composite += self._clip((close - vwap) / atr, Decimal(2))
if rsi < 30 and composite > 0:
composite += Decimal(1.5)
elif rsi < 40 and composite > 0:
composite += Decimal(0.5)
elif rsi > 70 and composite < 0:
composite -= Decimal(1.5)
elif rsi > 60 and composite < 0:
composite -= Decimal(0.5)
composite += self._clip((macd_value.Macd - macd_value.Signal) / atr, Decimal(1))
composite += self._clip((close - self._closes[0]) / atr, Decimal(1))
pending = self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed)
if self.Position != 0 or pending or self._last_entry is not None and self._bar - self._last_entry <= self.CooldownBars or not self.IsFormedAndOnlineAndAllowTrading() or abs(composite) < self.SignalThreshold:
return
self.StartProtection(Unit(), Unit(atr * self.AtrStopMultiplier, UnitTypes.Absolute), useMarketOrders=True, isLocalStop=True)
self._last_entry = self._bar
self._pending = self.CreateOrder(Sides.Buy if composite > 0 else Sides.Sell, Decimal(0), self.Volume)
self._pending.Type = OrderTypes.Market
self._pending.Comment = "0522Entry"
self.RegisterOrder(self._pending)
def CreateClone(self):
return anomalous_holonomy_field_theory_strategy()