在 GitHub 上查看

e-Skoch 挂单策略

概述

e-Skoch 挂单策略 复刻了原始的 MetaTrader 专家顾问。策略在每个新 K 线形成时,比较交易时间框架与日线时间框架的最近两根 K 线高低点,并在突破水平附近放置突破型挂单。目标是在日线趋势确认的情况下,捕捉价格冲破上一根 K 线后的动能。

本 StockSharp 版本沿用了原始思路,但使用了高层 API,包括蜡烛订阅、自动保护单和参数系统。C# 实现位于 CS/ 目录,目前尚未提供 Python 版本。

交易逻辑

  1. 每当一个蜡烛收盘时,读取交易时间框架最近两根蜡烛的高低点,同时获取前两根日线蜡烛的高低点。
  2. 若最近一根日线高点低于前一根日线高点,并且上一根交易周期高点低于再前一根高点,则在最近高点上方加上缓冲距离处放置 买入止损 挂单。
  3. 若最近一根日线低点高于前一根日线低点,并且上一根交易周期低点高于再前一根低点,则在最近低点下方减去缓冲距离处放置 卖出止损 挂单。
  4. 每个挂单都带有独立的止损和止盈。当挂单被触发后,策略会立即为当前持仓提交对应方向的保护性止损与止盈委托。
  5. 当没有持仓与挂单时,记录当前权益作为基准值;若账户权益相对该基准值的涨幅达到设定百分比,立即平掉所有持仓并取消保护性委托。
  6. 可选的 CheckExistingTrade 参数可以阻止在有持仓时继续发出新的挂单,行为与原始 EA 的 “CheckTrade” 参数一致。

参数说明

参数 描述
CandleType 用于产生信号的主时间框架,默认 1 小时蜡烛。
TakeProfitBuyPips / StopLossBuyPips 多头方向的止盈与止损偏移量(以点数计)。
TakeProfitSellPips / StopLossSellPips 空头方向的止盈与止损偏移量(以点数计)。
IndentHighPips / IndentLowPips 挂单距离最近高点或低点的缓冲点数。
CheckExistingTrade 为 true 时,只要存在持仓就不会放置新的挂单。
PercentEquity 相对基准权益的百分比增幅,达到时关闭全部仓位。
Volume 下单数量,默认 0.01 手以贴合原 EA 设置。

风险管理

  • 买入止损单会在入场价下方放置止损,在入场价上方放置止盈。
  • 卖出止损单会在入场价上方放置止损,在入场价下方放置止盈。
  • 当持仓平仓或生成新的保护组合时,会自动取消旧的保护性委托。
  • 权益增幅检测相当于全局止盈,在达到目标后立即锁定盈利并重新等待下一次机会。

注意事项

  • 策略需要同时订阅主时间框架与日线蜡烛,请确保在 Designer 或回测环境中具备这两类数据。
  • 对于采用 3 位或 5 位小数报价的外汇品种,策略会自动把价格步长乘以 10 以转换成标准点值。
  • CheckExistingTrade 启用时策略默认只维持单方向持仓,不会同时持有多空仓位。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pending breakout strategy based on the e-Skoch pending orders idea.
/// Detects falling highs or rising lows across two timeframes to enter on breakouts.
/// </summary>
public class ESkochPendingOrdersStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitBuyPips;
	private readonly StrategyParam<decimal> _stopLossBuyPips;
	private readonly StrategyParam<decimal> _takeProfitSellPips;
	private readonly StrategyParam<decimal> _stopLossSellPips;
	private readonly StrategyParam<decimal> _indentHighPips;
	private readonly StrategyParam<decimal> _indentLowPips;
	private readonly StrategyParam<bool> _checkExistingTrade;

	private decimal? _prevHigh1;
	private decimal? _prevHigh2;
	private decimal? _prevLow1;
	private decimal? _prevLow2;

	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;

	private decimal _entryPrice;
	private decimal _longStop;
	private decimal _longTake;
	private decimal _shortStop;
	private decimal _shortTake;

	private decimal _pipValue;

	/// <summary>
	/// Main candle type for signal evaluation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal TakeProfitBuyPips
	{
		get => _takeProfitBuyPips.Value;
		set => _takeProfitBuyPips.Value = value;
	}

	public decimal StopLossBuyPips
	{
		get => _stopLossBuyPips.Value;
		set => _stopLossBuyPips.Value = value;
	}

	public decimal TakeProfitSellPips
	{
		get => _takeProfitSellPips.Value;
		set => _takeProfitSellPips.Value = value;
	}

	public decimal StopLossSellPips
	{
		get => _stopLossSellPips.Value;
		set => _stopLossSellPips.Value = value;
	}

	public decimal IndentHighPips
	{
		get => _indentHighPips.Value;
		set => _indentHighPips.Value = value;
	}

	public decimal IndentLowPips
	{
		get => _indentLowPips.Value;
		set => _indentLowPips.Value = value;
	}

	public bool CheckExistingTrade
	{
		get => _checkExistingTrade.Value;
		set => _checkExistingTrade.Value = value;
	}

	public ESkochPendingOrdersStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");

		_takeProfitBuyPips = Param(nameof(TakeProfitBuyPips), 2000m)
			.SetGreaterThanZero()
			.SetDisplay("Buy TP (pips)", "Long take profit distance", "Trading");

		_stopLossBuyPips = Param(nameof(StopLossBuyPips), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Buy SL (pips)", "Long stop loss distance", "Trading");

		_takeProfitSellPips = Param(nameof(TakeProfitSellPips), 2000m)
			.SetGreaterThanZero()
			.SetDisplay("Sell TP (pips)", "Short take profit distance", "Trading");

		_stopLossSellPips = Param(nameof(StopLossSellPips), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Sell SL (pips)", "Short stop loss distance", "Trading");

		_indentHighPips = Param(nameof(IndentHighPips), 500m)
			.SetGreaterThanZero()
			.SetDisplay("High Indent", "Buy stop offset", "Trading");

		_indentLowPips = Param(nameof(IndentLowPips), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Low Indent", "Sell stop offset", "Trading");

		_checkExistingTrade = Param(nameof(CheckExistingTrade), true)
			.SetDisplay("Block During Position", "Skip signals when a position exists", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevHigh1 = null;
		_prevHigh2 = null;
		_prevLow1 = null;
		_prevLow2 = null;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
		_pipValue = 1m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var priceStep = Security?.PriceStep ?? 0m;
		_pipValue = priceStep <= 0m ? 1m : priceStep;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Check pending entries against current candle.
		CheckPendingEntries(candle);

		// Manage SL/TP for open positions.
		ManagePosition(candle);

		// Need at least 2 previous bars.
		if (_prevHigh1 is null)
		{
			_prevHigh1 = candle.HighPrice;
			_prevLow1 = candle.LowPrice;
			return;
		}

		if (_prevHigh2 is null)
		{
			_prevHigh2 = _prevHigh1;
			_prevLow2 = _prevLow1;
			_prevHigh1 = candle.HighPrice;
			_prevLow1 = candle.LowPrice;
			return;
		}

		var hasPosition = Position != 0;

		// Falling highs => place buy stop above recent high.
		if (_prevHigh2 > _prevHigh1 && !hasPosition)
		{
			if (!CheckExistingTrade || Position == 0)
			{
				var buyPrice = _prevHigh1.Value + _pipValue * IndentHighPips;
				_pendingBuyPrice = buyPrice;
				_longStop = buyPrice - _pipValue * StopLossBuyPips;
				_longTake = buyPrice + _pipValue * TakeProfitBuyPips;
			}
		}

		// Rising lows => place sell stop below recent low.
		if (_prevLow2 < _prevLow1 && !hasPosition)
		{
			if (!CheckExistingTrade || Position == 0)
			{
				var sellPrice = _prevLow1.Value - _pipValue * IndentLowPips;
				_pendingSellPrice = sellPrice;
				_shortStop = sellPrice + _pipValue * StopLossSellPips;
				_shortTake = sellPrice - _pipValue * TakeProfitSellPips;
			}
		}

		// Shift history.
		_prevHigh2 = _prevHigh1;
		_prevLow2 = _prevLow1;
		_prevHigh1 = candle.HighPrice;
		_prevLow1 = candle.LowPrice;
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (Position != 0)
			return;

		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
		{
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
		{
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop > 0m && candle.LowPrice <= _longStop)
			{
				SellMarket();
				ResetPositionState();
				return;
			}
			if (_longTake > 0m && candle.HighPrice >= _longTake)
			{
				SellMarket();
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop > 0m && candle.HighPrice >= _shortStop)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}
			if (_shortTake > 0m && candle.LowPrice <= _shortTake)
			{
				BuyMarket();
				ResetPositionState();
			}
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
	}
}