e-Skoch 挂单策略
概述
e-Skoch 挂单策略 复刻了原始的 MetaTrader 专家顾问。策略在每个新 K 线形成时,比较交易时间框架与日线时间框架的最近两根 K 线高低点,并在突破水平附近放置突破型挂单。目标是在日线趋势确认的情况下,捕捉价格冲破上一根 K 线后的动能。
本 StockSharp 版本沿用了原始思路,但使用了高层 API,包括蜡烛订阅、自动保护单和参数系统。C# 实现位于 CS/ 目录,目前尚未提供 Python 版本。
交易逻辑
- 每当一个蜡烛收盘时,读取交易时间框架最近两根蜡烛的高低点,同时获取前两根日线蜡烛的高低点。
- 若最近一根日线高点低于前一根日线高点,并且上一根交易周期高点低于再前一根高点,则在最近高点上方加上缓冲距离处放置 买入止损 挂单。
- 若最近一根日线低点高于前一根日线低点,并且上一根交易周期低点高于再前一根低点,则在最近低点下方减去缓冲距离处放置 卖出止损 挂单。
- 每个挂单都带有独立的止损和止盈。当挂单被触发后,策略会立即为当前持仓提交对应方向的保护性止损与止盈委托。
- 当没有持仓与挂单时,记录当前权益作为基准值;若账户权益相对该基准值的涨幅达到设定百分比,立即平掉所有持仓并取消保护性委托。
- 可选的
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class e_skoch_pending_orders_strategy(Strategy):
"""Pending breakout: detects falling highs or rising lows to enter on breakouts with SL/TP."""
def __init__(self):
super(e_skoch_pending_orders_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._take_profit_buy_pips = self.Param("TakeProfitBuyPips", 2000.0) \
.SetGreaterThanZero() \
.SetDisplay("Buy TP (pips)", "Long take profit distance", "Trading")
self._stop_loss_buy_pips = self.Param("StopLossBuyPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Buy SL (pips)", "Long stop loss distance", "Trading")
self._take_profit_sell_pips = self.Param("TakeProfitSellPips", 2000.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell TP (pips)", "Short take profit distance", "Trading")
self._stop_loss_sell_pips = self.Param("StopLossSellPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell SL (pips)", "Short stop loss distance", "Trading")
self._indent_high_pips = self.Param("IndentHighPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("High Indent", "Buy stop offset", "Trading")
self._indent_low_pips = self.Param("IndentLowPips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Low Indent", "Sell stop offset", "Trading")
self._check_existing_trade = self.Param("CheckExistingTrade", True) \
.SetDisplay("Block During Position", "Skip signals when a position exists", "Risk")
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pip_value = 1.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def TakeProfitBuyPips(self):
return float(self._take_profit_buy_pips.Value)
@property
def StopLossBuyPips(self):
return float(self._stop_loss_buy_pips.Value)
@property
def TakeProfitSellPips(self):
return float(self._take_profit_sell_pips.Value)
@property
def StopLossSellPips(self):
return float(self._stop_loss_sell_pips.Value)
@property
def IndentHighPips(self):
return float(self._indent_high_pips.Value)
@property
def IndentLowPips(self):
return float(self._indent_low_pips.Value)
@property
def CheckExistingTrade(self):
return self._check_existing_trade.Value
def OnStarted2(self, time):
super(e_skoch_pending_orders_strategy, self).OnStarted2(time)
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
self._pip_value = price_step if price_step > 0 else 1.0
subscription = self.SubscribeCandles(self.CandleType)
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
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
# Check pending entries
self._check_pending_entries(h, lo)
# Manage SL/TP
self._manage_position(h, lo)
# Need at least 2 previous bars
if self._prev_high1 is None:
self._prev_high1 = h
self._prev_low1 = lo
return
if self._prev_high2 is None:
self._prev_high2 = self._prev_high1
self._prev_low2 = self._prev_low1
self._prev_high1 = h
self._prev_low1 = lo
return
has_position = self.Position != 0
# Falling highs -> place buy stop above recent high
if self._prev_high2 > self._prev_high1 and not has_position:
if not self.CheckExistingTrade or self.Position == 0:
buy_price = self._prev_high1 + self._pip_value * self.IndentHighPips
self._pending_buy_price = buy_price
self._long_stop = buy_price - self._pip_value * self.StopLossBuyPips
self._long_take = buy_price + self._pip_value * self.TakeProfitBuyPips
# Rising lows -> place sell stop below recent low
if self._prev_low2 < self._prev_low1 and not has_position:
if not self.CheckExistingTrade or self.Position == 0:
sell_price = self._prev_low1 - self._pip_value * self.IndentLowPips
self._pending_sell_price = sell_price
self._short_stop = sell_price + self._pip_value * self.StopLossSellPips
self._short_take = sell_price - self._pip_value * self.TakeProfitSellPips
# Shift history
self._prev_high2 = self._prev_high1
self._prev_low2 = self._prev_low1
self._prev_high1 = h
self._prev_low1 = lo
def _check_pending_entries(self, h, lo):
if self.Position != 0:
return
if self._pending_buy_price is not None and h >= self._pending_buy_price:
self.BuyMarket()
self._entry_price = self._pending_buy_price
self._pending_buy_price = None
self._pending_sell_price = None
return
if self._pending_sell_price is not None and lo <= self._pending_sell_price:
self.SellMarket()
self._entry_price = self._pending_sell_price
self._pending_buy_price = None
self._pending_sell_price = None
def _manage_position(self, h, lo):
if self.Position > 0:
if self._long_stop > 0 and lo <= self._long_stop:
self.SellMarket()
self._reset_position_state()
return
if self._long_take > 0 and h >= self._long_take:
self.SellMarket()
self._reset_position_state()
elif self.Position < 0:
if self._short_stop > 0 and h >= self._short_stop:
self.BuyMarket()
self._reset_position_state()
return
if self._short_take > 0 and lo <= self._short_take:
self.BuyMarket()
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pending_buy_price = None
self._pending_sell_price = None
def OnReseted(self):
super(e_skoch_pending_orders_strategy, self).OnReseted()
self._prev_high1 = None
self._prev_high2 = None
self._prev_low1 = None
self._prev_low2 = None
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = 0.0
self._long_stop = 0.0
self._long_take = 0.0
self._short_stop = 0.0
self._short_take = 0.0
self._pip_value = 1.0
def CreateClone(self):
return e_skoch_pending_orders_strategy()