Anomalous Holonomy Field Theory
六部分技术信号:固定 EMA20/EMA50、Wilder RSI14、MACD(12,26,9)、Wilder ATR 及按波动率归一化的 ROC。C 为原始收盘,A=ATR;clip(x,k)=max(-k,min(k,x))。窗口包含当前已完成蜡烛。
交易规则
T:高于两条 EMA 时 +1.5,仅高于 EMA20 时 +1;低于两条时 -1.5,仅低于 EMA20 时 -1,否则 0。蜡烛 VWAP 为最近 VwapPeriod 根的 sum(((High+Low+Close)/3)*Volume)/sum(Volume)。V=clip((C-VWAP)/A,2)。令 B=T+V:仅当 B>0,RSI 小于 30 加 +1.5,小于 40 加 +0.5;仅当 B<0,RSI 大于 70 加 -1.5,大于 60 加 -0.5;其余 R=0。
M=clip((MACD-EMA9(MACD))/A,1);Q=clip((C-C[RocPeriod 根前])/A,1) 等于使用相同历史价格基准的 ROC/ATR%。总信号 S=T+V+R+M+Q。仅空仓时,S>=SignalThreshold 买入 Volume,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()