在 GitHub 上查看
21hour 策略
概述
21hour 策略复刻了 MQL4 专家顾问 21hour.mq4 的逻辑。它围绕每日的时间窗口运行:在设定的启动小时放置一对突破型止损挂单,并在设定的结束小时撤销所有敞口。StockSharp 版本保持“双向止损挂单包围价格”的思路,同时借助高级 API 处理行情订阅、下单及自动化的止盈管理。
交易逻辑
- 每个交易日开始时,当服务器时间到达
StartHour:00,策略会读取最新的买卖报价,同时放置 buy stop 和 sell stop。
- buy stop 的触发价位于当前 ask 之上,距离为
StepPoints * PriceStep。
- sell stop 的触发价位于当前 bid 之下,距离相同。
TakeProfitPoints 通过合约的最小跳动转换为价格距离并传给 StartProtection,因此多头和空头持仓在成交后立即获得保护性的止盈单。
- 每天只允许存在一组挂单。如果只剩下单侧挂单处于激活状态(例如另一侧已成交),策略会撤销剩余挂单,以符合原始 EA 的行为。
- 当时间到达
StopHour:00 时,策略会以市价平掉所有持仓,并撤销所有未成交挂单,即使突破没有发生也会执行。
- 默认使用一分钟 K 线流,仅用于在收盘蜡烛上触发整点检查,等同于 MQL 中基于
prevtime 的防重逻辑。
参数
| 参数 |
说明 |
默认值 |
Volume |
两个挂单使用的下单手数。 |
0.1 |
StartHour |
生成挂单的小时(0–23)。 |
10 |
StopHour |
平仓并撤单的小时(0–23)。 |
22 |
StepPoints |
当前 bid/ask 与止损挂单触发价之间的点数距离,通过 PriceStep 转换为价格。 |
15 |
TakeProfitPoints |
从成交价到止盈目标的点数距离,由 StartProtection 管理;0 表示不使用止盈。 |
200 |
CandleType |
用于时间检测的蜡烛类型,默认是一分钟时间框架(TimeSpan.FromMinutes(1).TimeFrame())。 |
1 分钟 |
实现说明
- 使用
SubscribeCandles 订阅蜡烛并在每根收盘蜡烛后检查时间窗口,避免重复执行。
- 通过
SubscribeLevel1() 订阅一级行情以获取最新的 bid/ask,用于精确计算挂单价格。
- 调用
StartProtection 并传入止盈距离,从而模拟原始 EA 中附加在挂单上的止盈功能,而无需手动管理保护单。
- 维护 buy stop 与 sell stop 的引用,如果只剩一侧处于激活状态则调用
CancelOrder 撤销它,确保不会遗留单侧挂单。
- 在停止时间使用
BuyMarket / SellMarket 等高级方法平仓,完全依赖 StockSharp 策略 API。
其他说明
- 策略需要经纪商连接提供
PriceStep 信息;如果无法获得,则不会对价格进行步长四舍五入。
- 每个自然日仅生成一次挂单组合;即便前一日的突破未成交,次日到达启动时间后仍会重新挂单。
- 当
TakeProfitPoints 设为 0 时,策略仍会挂出突破单,但不会再自动管理止盈。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Time-based breakout strategy. At start hour, detects breakout direction from previous candle range.
/// At stop hour, closes all positions.
/// </summary>
public class TwentyOneHourStrategy : Strategy
{
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _stopHour;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevHigh;
private decimal _prevLow;
private bool _hasPrev;
private bool _tradedToday;
private int _lastTradeDay;
public TwentyOneHourStrategy()
{
_startHour = Param(nameof(StartHour), 10)
.SetDisplay("Start Hour", "Hour to look for breakout entries.", "Schedule");
_stopHour = Param(nameof(StopHour), 22)
.SetDisplay("Stop Hour", "Hour to close positions.", "Schedule");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candles used for time tracking.", "General");
}
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
public int StopHour
{
get => _stopHour.Value;
set => _stopHour.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevHigh = 0;
_prevLow = 0;
_hasPrev = false;
_tradedToday = false;
_lastTradeDay = -1;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevHigh = 0;
_prevLow = 0;
_hasPrev = false;
_tradedToday = false;
_lastTradeDay = -1;
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;
var hour = candle.OpenTime.Hour;
var day = candle.OpenTime.DayOfYear;
// Reset daily flag
if (day != _lastTradeDay)
{
_tradedToday = false;
_lastTradeDay = day;
}
// Close at stop hour
if (hour >= StopHour && Position != 0)
{
if (Position > 0)
SellMarket();
else
BuyMarket();
}
// Entry at start hour window
if (hour >= StartHour && hour < StopHour && !_tradedToday && _hasPrev && Position == 0)
{
if (candle.ClosePrice > _prevHigh)
{
BuyMarket();
_tradedToday = true;
}
else if (candle.ClosePrice < _prevLow)
{
SellMarket();
_tradedToday = true;
}
}
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_hasPrev = true;
}
}
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 twenty_one_hour_strategy(Strategy):
"""Time-based breakout: enter on breakout from previous candle range, close at stop hour."""
def __init__(self):
super(twenty_one_hour_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 10).SetDisplay("Start Hour", "Hour to look for breakout entries", "Schedule")
self._stop_hour = self.Param("StopHour", 22).SetDisplay("Stop Hour", "Hour to close positions", "Schedule")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(twenty_one_hour_strategy, self).OnReseted()
self._prev_high = 0
self._prev_low = 0
self._has_prev = False
self._traded_today = False
self._last_trade_day = -1
def OnStarted2(self, time):
super(twenty_one_hour_strategy, self).OnStarted2(time)
self._prev_high = 0
self._prev_low = 0
self._has_prev = False
self._traded_today = False
self._last_trade_day = -1
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
hour = candle.OpenTime.Hour
day = candle.OpenTime.DayOfYear
# Reset daily flag
if day != self._last_trade_day:
self._traded_today = False
self._last_trade_day = day
# Close at stop hour
start_h = self._start_hour.Value
stop_h = self._stop_hour.Value
if hour >= stop_h and self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
# Entry at start hour window
if hour >= start_h and hour < stop_h and not self._traded_today and self._has_prev and self.Position == 0:
close = float(candle.ClosePrice)
if close > self._prev_high:
self.BuyMarket()
self._traded_today = True
elif close < self._prev_low:
self.SellMarket()
self._traded_today = True
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_prev = True
def CreateClone(self):
return twenty_one_hour_strategy()