在 GitHub 上查看

日内交易ES影线长度策略

当K线的上下影线总长度超过其加上偏移量的移动平均值时,该策略建立多头仓位,并在持有固定数量的K线后退出。

可检验的交易条件(C# 与 Python)

对每根已完成的1分钟K线,总影线长度 W = max(0, High − max(Open, Close)) + max(0, min(Open, Close) − Low)。采用当前K线之前的20根已完成K线计算 VWMA(W) = sum(Wᵢ × Volumeᵢ) / sum(Volumeᵢ),若全部成交量为零则回退到简单平均;MaType=Simple 直接使用算术平均。MaOffset=10 的单位是证券最小价格跳动,入场阈值为 VWMA(W) + 10 × Security.PriceStep。预热20根后,仅在空仓且 W > 阈值 时开多仓。经过之后18根已完成K线,卖出全部剩余持仓。不做空,不设置止损或止盈。默认 MaLength=20、MaType=VolumeWeighted、MaOffset=10、HoldPeriods=18、CandleType=1分钟。

细节

  • 入场条件:影线总长度大于带有偏移的移动平均。
  • 出场条件:持有 Hold periods 根K线后平仓。
  • 多空方向:仅做多。
  • 止损:无。
  • 默认值:
    • MA length = 20
    • MA type = VolumeWeighted
    • MA offset = 10
    • Hold periods = 18
    • Candle type = 1 分钟K线
  • 过滤器:
    • 类别:波动性
    • 方向:多头
    • 指标:移动平均、影线长度
    • 止损:无
    • 复杂度:简单
    • 时间框架:日内
    • 季节性:无
    • 神经网络:无
    • 背离:无
    • 风险级别:中等
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;
            }
        }
    }
}