在 GitHub 上查看
21小时时段突破策略
该策略在 StockSharp 中复现 MetaTrader 上的 “21hour” 专家顾问。它围绕两个可配置的交易时段运作:在每个时段开始时布置双向突破挂单,在时段结束时清空持仓并撤销所有挂单,确保新时段从零仓位开始。
核心思想
- 完全依赖时间驱动:仅在指定小时开始时寻找突破机会。
- 时段启动时,在当前最优卖价之上放置买入止损挂单,在最优买价之下放置卖出止损挂单,距离由
StepPoints 控制。
- 任一挂单成交后立即取消另一侧挂单,同时按
TakeProfitPoints 设置固定距离的止盈限价单。
- 到达时段结束时间后,无论盈亏,都会市价平仓并撤销所有剩余挂单。
数据来源
- K线: 默认使用 1 分钟 K 线(可通过
CandleType 调整)来触发时间检查。
- 盘口: 订阅 Level 1 行情以获取实时最优买价/卖价,计算挂单价格。
交易规则
入场逻辑
- 当时间到达
FirstSessionStartHour(默认 08:00)或 SecondSessionStartHour(默认 22:00)时:
- 在
Ask + StepPoints * PriceStep 处提交买入止损单。
- 在
Bid - StepPoints * PriceStep 处提交卖出止损单。
- 策略只允许一侧持仓,如果在新时段开始时已经持仓,会先取消现有挂单后重新布置。
持仓管理
- 某一侧挂单成交后立刻撤销另一侧挂单。
- 根据成交价在固定距离处挂出止盈限价单。
- 所有订单的数量由
Volume 控制,默认 1 手。
离场逻辑
- 止盈单触发后自动平仓。
- 到达
FirstSessionStopHour(默认 21:00)或 SecondSessionStopHour(默认 23:00)时,强制市价平仓并撤销所有订单。
- 如果仓位被外部平掉,策略会自动撤销尚未成交的止盈单。
参数说明
| 参数 |
默认值 |
说明 |
Volume |
1 |
每笔订单使用的固定手数。 |
FirstSessionStartHour |
8 |
第一交易时段开始的小时(0-23)。 |
FirstSessionStopHour |
21 |
第一交易时段结束的小时。 |
SecondSessionStartHour |
22 |
第二交易时段开始的小时,必须晚于第一时段开始。 |
SecondSessionStopHour |
23 |
第二交易时段结束的小时,必须晚于第一时段结束。 |
StepPoints |
5 |
距离最优价的止损挂单间隔(价格步长的倍数)。 |
TakeProfitPoints |
40 |
入场价格到止盈价格的距离(价格步长的倍数)。 |
CandleType |
1 分钟 |
用于驱动时间逻辑的 K 线类型。 |
参数在启动时会进行完整校验,防止时段重叠或非法的时间组合。
策略特性
- 风格: 时段突破 / 时间驱动趋势跟随。
- 方向: 做多和做空皆可。
- 周期: 日内,根据时间触发(K线仅用于计时)。
- 风险控制: 固定止盈 + 时段结束强制平仓(无显式止损)。
- 适用市场: 连续交易的外汇、指数及其他高流动性品种。
- 复杂度: 低,无技术指标计算。
实现细节
- 策略必须能够获取
Security.PriceStep;若缺少价格步长或报价则不会下单。
- 止盈单的数量优先使用实际成交量,其次为当前仓位,再次为配置的
Volume。
- 代码添加了英文注释,并使用 StockSharp 的高级 API(
SubscribeCandles、SubscribeOrderBook 等)来实现与原始 MQL 程序等价的逻辑。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// 21-hour session breakout strategy. Places simulated stop entries via candle breakout logic.
/// </summary>
public class TwentyOneHourSessionBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _firstSessionStartHour;
private readonly StrategyParam<int> _firstSessionStopHour;
private readonly StrategyParam<decimal> _stepPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal? _sessionOpen;
private decimal _entryPrice;
private bool _inSession;
public int FirstSessionStartHour
{
get => _firstSessionStartHour.Value;
set => _firstSessionStartHour.Value = value;
}
public int FirstSessionStopHour
{
get => _firstSessionStopHour.Value;
set => _firstSessionStopHour.Value = value;
}
public decimal StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TwentyOneHourSessionBreakoutStrategy()
{
_firstSessionStartHour = Param(nameof(FirstSessionStartHour), 2)
.SetDisplay("Session Start", "Hour of the trading window start", "Schedule");
_firstSessionStopHour = Param(nameof(FirstSessionStopHour), 20)
.SetDisplay("Session Stop", "Hour of the trading window stop", "Schedule");
_stepPoints = Param(nameof(StepPoints), 40m)
.SetGreaterThanZero()
.SetDisplay("Step Points", "Distance from session open to breakout level", "Orders");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
.SetGreaterThanZero()
.SetDisplay("Take Profit Points", "Take-profit distance", "Orders");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles used to drive the trading schedule", "Data");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_sessionOpen = null;
_entryPrice = 0m;
_inSession = false;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hour = candle.OpenTime.Hour;
var priceStep = Security?.PriceStep ?? 1m;
// Session start: record the open price
if (hour >= FirstSessionStartHour && hour < FirstSessionStopHour)
{
if (!_inSession)
{
_sessionOpen = candle.OpenPrice;
_inSession = true;
}
if (_sessionOpen == null)
return;
var stepOffset = StepPoints * priceStep;
var buyLevel = _sessionOpen.Value + stepOffset;
var sellLevel = _sessionOpen.Value - stepOffset;
// Breakout entry
if (Position == 0)
{
if (candle.HighPrice >= buyLevel)
{
BuyMarket();
_entryPrice = buyLevel;
}
else if (candle.LowPrice <= sellLevel)
{
SellMarket();
_entryPrice = sellLevel;
}
}
// Take profit
if (Position > 0)
{
var tp = _entryPrice + TakeProfitPoints * priceStep;
if (candle.HighPrice >= tp)
{
SellMarket();
_sessionOpen = candle.ClosePrice;
}
}
else if (Position < 0)
{
var tp = _entryPrice - TakeProfitPoints * priceStep;
if (candle.LowPrice <= tp)
{
BuyMarket();
_sessionOpen = candle.ClosePrice;
}
}
}
else
{
// Session end: close position
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
_inSession = false;
_sessionOpen = 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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class twenty_one_hour_session_breakout_strategy(Strategy):
def __init__(self):
super(twenty_one_hour_session_breakout_strategy, self).__init__()
self._first_session_start_hour = self.Param("FirstSessionStartHour", 2)
self._first_session_stop_hour = self.Param("FirstSessionStopHour", 20)
self._step_points = self.Param("StepPoints", 40.0)
self._take_profit_points = self.Param("TakeProfitPoints", 200.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._session_open = None
self._entry_price = 0.0
self._in_session = False
@property
def FirstSessionStartHour(self):
return self._first_session_start_hour.Value
@FirstSessionStartHour.setter
def FirstSessionStartHour(self, value):
self._first_session_start_hour.Value = value
@property
def FirstSessionStopHour(self):
return self._first_session_stop_hour.Value
@FirstSessionStopHour.setter
def FirstSessionStopHour(self, value):
self._first_session_stop_hour.Value = value
@property
def StepPoints(self):
return self._step_points.Value
@StepPoints.setter
def StepPoints(self, value):
self._step_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(twenty_one_hour_session_breakout_strategy, self).OnStarted2(time)
self._session_open = None
self._entry_price = 0.0
self._in_session = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
hour = candle.OpenTime.Hour
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if price_step <= 0.0:
price_step = 1.0
start_hour = int(self.FirstSessionStartHour)
stop_hour = int(self.FirstSessionStopHour)
if hour >= start_hour and hour < stop_hour:
if not self._in_session:
self._session_open = float(candle.OpenPrice)
self._in_session = True
if self._session_open is None:
return
step_offset = float(self.StepPoints) * price_step
buy_level = self._session_open + step_offset
sell_level = self._session_open - step_offset
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position == 0:
if high >= buy_level:
self.BuyMarket()
self._entry_price = buy_level
elif low <= sell_level:
self.SellMarket()
self._entry_price = sell_level
if self.Position > 0:
tp = self._entry_price + float(self.TakeProfitPoints) * price_step
if high >= tp:
self.SellMarket()
self._session_open = close
elif self.Position < 0:
tp = self._entry_price - float(self.TakeProfitPoints) * price_step
if low <= tp:
self.BuyMarket()
self._session_open = close
else:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._in_session = False
self._session_open = None
def OnReseted(self):
super(twenty_one_hour_session_breakout_strategy, self).OnReseted()
self._session_open = None
self._entry_price = 0.0
self._in_session = False
def CreateClone(self):
return twenty_one_hour_session_breakout_strategy()