Estrategia de Daytrading ES por Longitud de Mecha
Esta estrategia entra en una posición larga cuando la longitud total de la mecha de una vela supera su media móvil más un desplazamiento, y sale después de mantener la posición durante un número fijo de barras.
Reglas verificables (C# y Python)
En cada vela terminada de un minuto, la mecha total es W = max(0, High − max(Open, Close)) + max(0, min(Open, Close) − Low). Se usan las 20 velas anteriores, sin incluir la actual, para calcular VWMA(W) = sum(Wᵢ × Volumeᵢ) / sum(Volumeᵢ); si todo el volumen es cero se utiliza la media simple. MaType=Simple usa siempre la media sin ponderación. MaOffset=10 representa 10 ticks mínimos del instrumento; umbral VWMA(W) + 10 × Security.PriceStep. Después de 20 velas de preparación, se compra únicamente desde posición plana cuando W > umbral. Tras 18 velas terminadas adicionales, se vende toda la posición restante. Sin cortos, stop-loss ni take-profit. Predeterminados: MaLength=20, MaType=VolumeWeighted, MaOffset=10, HoldPeriods=18, CandleType=1 minuto.
Detalles
- Criterios de entrada: Longitud total de mecha mayor que la media móvil con desplazamiento.
- Criterios de salida: Posición cerrada después de mantener
Hold periods barras.
- Largo/Corto: Solo largos.
- Stops: Ninguno.
- Valores predeterminados:
MA length = 20
MA type = VolumeWeighted
MA offset = 10
Hold periods = 18
Candle type = velas de 1 minuto
- Filtros:
- Categoría: Volatilidad
- Dirección: Largo
- Indicadores: Moving Average, longitud de mecha
- Stops: No
- Complejidad: Simple
- Marco temporal: Intradía
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Long-only wick-length strategy. Compares total candle-shadow length with
/// the preceding bars' volume-weighted (or simple) shadow average plus a
/// configurable offset in the traded security's price ticks.
/// </summary>
public class DaytradingESWickLengthStrategy : Strategy
{
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<string> _maType;
private readonly StrategyParam<decimal> _maOffset;
private readonly StrategyParam<int> _holdPeriods;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal wick, decimal volume)> _samples = [];
private int _finishedBars;
private int _entryBar;
public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
public string MaType { get => _maType.Value; set => _maType.Value = value; }
public decimal MaOffset { get => _maOffset.Value; set => _maOffset.Value = value; }
public int HoldPeriods { get => _holdPeriods.Value; set => _holdPeriods.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DaytradingESWickLengthStrategy()
{
_maLength = Param(nameof(MaLength), 20)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Number of preceding completed candles used for the wick average", "Indicators");
_maType = Param(nameof(MaType), "VolumeWeighted")
.SetDisplay("MA Type", "VolumeWeighted or Simple average of preceding candle wicks", "Indicators");
_maOffset = Param(nameof(MaOffset), 10m)
.SetRange(0m, 100000m)
.SetDisplay("MA Offset", "Wick-length threshold offset, measured in security price ticks", "Signals");
_holdPeriods = Param(nameof(HoldPeriods), 18)
.SetGreaterThanZero()
.SetDisplay("Hold Periods", "Completed candles to hold a filled long position", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Signal candle interval", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_samples.Clear();
_finishedBars = 0;
_entryBar = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (MaType != "VolumeWeighted" && MaType != "Simple")
throw new ArgumentException("MaType must be VolumeWeighted or Simple.", nameof(MaType));
_samples.Clear();
_finishedBars = 0;
_entryBar = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
/// <summary>Combined upper and lower candle-shadow length.</summary>
public static decimal CalculateWickLength(decimal open, decimal high, decimal low, decimal close)
=> Math.Max(0m, high - Math.Max(open, close)) +
Math.Max(0m, Math.Min(open, close) - low);
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_finishedBars++;
var wick = CalculateWickLength(candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice);
// Compute the threshold from completed PRIOR candles, without the
// current signal candle leaking into its own volume-weighted average.
var formed = _samples.Count >= MaLength;
decimal average = 0m;
if (formed)
{
decimal sumWick = 0m;
decimal sumVolume = 0m;
decimal weightedWick = 0m;
foreach (var sample in _samples)
{
sumWick += sample.wick;
sumVolume += sample.volume;
weightedWick += sample.wick * sample.volume;
}
average = MaType == "VolumeWeighted" && sumVolume > 0m
? weightedWick / sumVolume
: sumWick / _samples.Count;
}
_samples.Add((wick, Math.Max(0m, candle.TotalVolume)));
if (_samples.Count > MaLength)
_samples.RemoveAt(0);
if (!formed || !IsFormedAndOnlineAndAllowTrading())
return;
// Exits are independent of new wick signals; close the ACTUAL residual
// size, not the nominal default order size.
if (Position > 0m)
{
if (_entryBar > 0 && _finishedBars - _entryBar >= HoldPeriods)
{
SellMarket(Position);
_entryBar = 0;
}
}
else if (Position == 0m && _entryBar == 0)
{
var tick = Security?.PriceStep ?? 0m;
if (tick <= 0m)
return;
if (wick > average + MaOffset * tick)
{
BuyMarket();
_entryBar = _finishedBars;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class daytrading_es_wick_length_strategy(Strategy):
"""Long-only total-wick strategy using a preceding volume-weighted wick average."""
def __init__(self):
super(daytrading_es_wick_length_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("MA Length", "Preceding completed candles for wick average", "Indicators")
self._ma_type = self.Param("MaType", "VolumeWeighted") \
.SetDisplay("MA Type", "VolumeWeighted or Simple", "Indicators")
self._ma_offset = self.Param("MaOffset", 10.0) \
.SetRange(0.0, 100000.0) \
.SetDisplay("MA Offset", "Additional wick length in security price ticks", "Signals")
self._hold_periods = self.Param("HoldPeriods", 18) \
.SetGreaterThanZero() \
.SetDisplay("Hold Periods", "Completed candles to hold an open long", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Signal candle interval", "General")
self._samples = []
self._finished_bars = 0
self._entry_bar = 0
@property
def candle_type(self):
return self._candle_type.Value
@staticmethod
def calculate_wick_length(open_price, high, low, close):
return max(0.0, high - max(open_price, close)) + max(0.0, min(open_price, close) - low)
def OnReseted(self):
super(daytrading_es_wick_length_strategy, self).OnReseted()
self._samples = []
self._finished_bars = 0
self._entry_bar = 0
def OnStarted2(self, time):
super(daytrading_es_wick_length_strategy, self).OnStarted2(time)
if str(self._ma_type.Value) not in ("VolumeWeighted", "Simple"):
raise ValueError("MaType must be VolumeWeighted or Simple")
self._samples = []
self._finished_bars = 0
self._entry_bar = 0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._finished_bars += 1
wick = self.calculate_wick_length(
float(candle.OpenPrice), float(candle.HighPrice),
float(candle.LowPrice), float(candle.ClosePrice)
)
ma_length = int(self._ma_length.Value)
formed = len(self._samples) >= ma_length
average = 0.0
if formed:
sum_wick = sum(sample[0] for sample in self._samples)
sum_volume = sum(sample[1] for sample in self._samples)
weighted_wick = sum(sample[0] * sample[1] for sample in self._samples)
average = (weighted_wick / sum_volume) if (
str(self._ma_type.Value) == "VolumeWeighted" and sum_volume > 0
) else sum_wick / len(self._samples)
self._samples.append((wick, max(0.0, float(candle.TotalVolume))))
if len(self._samples) > ma_length:
self._samples.pop(0)
if not formed or not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position > 0:
if self._entry_bar > 0 and self._finished_bars - self._entry_bar >= int(self._hold_periods.Value):
self.SellMarket(self.Position)
self._entry_bar = 0
elif self.Position == 0 and self._entry_bar == 0:
price_step = float(self.Security.PriceStep) if self.Security is not None else 0.0
if price_step <= 0:
return
if wick > average + float(self._ma_offset.Value) * price_step:
self.BuyMarket()
self._entry_bar = self._finished_bars
def CreateClone(self):
return daytrading_es_wick_length_strategy()