Hans123 Trader 策略
策略概述
Hans123 Trader 是将 MetaTrader 5 上的 Hans123_Trader 专家顾问移植到 StockSharp 的突破策略。策略在设定的时间窗口内监控最近 RangeLength 根蜡烛的最高价与最低价,并在区间边界上刷新买入/卖出止损挂单,同时支持止损、止盈以及追踪止损。
工作原理
- 订阅指定周期的蜡烛数据,等待
RangeLength个柱子形成完整的通道。 - 每当有新的完整蜡烛产生时:
- 更新最近
RangeLength根蜡烛的最高价与最低价。 - 若当前时间不在
[StartHour, EndHour)区间内,取消所有挂单并跳过本根蜡烛。 - 在区间内则撤销旧的挂单,并重新下单:
- 在最高价处挂出
OrderVolume手的买入止损单; - 在最低价处挂出
OrderVolume手的卖出止损单。
- 在最高价处挂出
- 更新最近
- 当任一挂单成交后:
- 取消方向相反的挂单,避免双向持仓;
- 如果设置了止损或止盈距离,则按照点差转换成价格,分别提交对冲方向的止损/止盈挂单。
- 在持仓期间:
- 若价格较开仓价移动了
TrailingStopPips + TrailingStepPips点,则将止损价格向趋势方向推进TrailingStopPips点,实现保本或锁盈; - 当仓位回到零时,自动撤销所有保护单并清空内部状态。
- 若价格较开仓价移动了
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
OrderVolume |
突破时下单的手数/数量。 | 0.1 |
RangeLength |
计算突破通道的蜡烛数量。 | 80 |
StopLossPips |
止损距离(点)。填 0 表示不开启。 | 50 |
TakeProfitPips |
止盈距离(点)。填 0 表示不开启。 | 50 |
TrailingStopPips |
追踪止损基础距离(点)。填 0 表示关闭追踪。 | 10 |
TrailingStepPips |
追踪止损再次触发所需的额外点数。追踪开启时必须为正数。 | 5 |
StartHour |
允许挂单的起始小时(UTC,含)。 | 6 |
EndHour |
允许挂单的结束小时(UTC,不含)。 | 10 |
CandleType |
计算所用的蜡烛类型/周期。 | 1 小时 |
实践建议
CalculatePipSize会根据证券的小数位自动调整 pip 大小,外汇类 3/5 位报价会得到 ×10 的补偿。- 如果
StopLossPips为 0,但启用了追踪止损,则首次不会生成保护单,只有当价格达到激活距离后才会提交新的止损单。 - 请确保投资组合的最小/最大成交量限制与
OrderVolume相匹配。 - 策略会绘制蜡烛、通道以及成交记录,方便在图表中验证逻辑。
与 MQL5 版本的差异
- 使用 StockSharp 的
BuyStop/SellStop等高层 API 注册挂单,而非 MetaTrader 的交易请求接口。 - 参数通过
StrategyParam暴露,可直接用于优化器,与原版默认值保持一致。 - 由于 StockSharp 基于蜡烛完成事件触发,挂单会在每根完成的蜡烛上更新,而非逐 Tick 轮询。
使用步骤
- 将策略绑定到目标证券和投资组合,确认蜡烛周期与原始策略设定一致。
- 根据交易品种波动性调整止损/止盈和交易时间窗口。
- 启动策略并在图表上观察通道与成交标记,确保逻辑符合预期。
- 如需回测或优化,可直接使用参数元数据在 StockSharp 测试框架中运行网格搜索。
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>
/// Hans123 breakout strategy converted from MQL5.
/// Collects an intraday range and trades pending stop orders within a trading window.
/// Applies configurable stop-loss, take-profit, and trailing protection.
/// </summary>
public class Hans123TraderStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _rangeLength;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<DataType> _candleType;
private Highest _highest = null!;
private Lowest _lowest = null!;
private decimal _entryPrice;
private decimal _pipSize;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
/// <summary>
/// Volume used for breakout orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Number of candles that form the breakout range.
/// </summary>
public int RangeLength
{
get => _rangeLength.Value;
set => _rangeLength.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Extra move (in pips) before trailing activates again.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Start hour (inclusive) of the trading window.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// End hour (exclusive) of the trading window.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Hans123TraderStrategy"/> class.
/// </summary>
public Hans123TraderStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetDisplay("Order Volume", "Breakout order volume", "General")
.SetOptimize(0.1m, 2m, 0.1m);
_rangeLength = Param(nameof(RangeLength), 40)
.SetGreaterThanZero()
.SetDisplay("Range Length", "Candles in breakout range", "General")
.SetOptimize(40, 120, 10);
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
.SetOptimize(0, 150, 10);
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
.SetOptimize(0, 200, 10);
_trailingStopPips = Param(nameof(TrailingStopPips), 10)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk Management")
.SetOptimize(0, 100, 5);
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetDisplay("Trailing Step (pips)", "Extra pips before trailing updates", "Risk Management")
.SetOptimize(0, 50, 5);
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Hour (UTC) when orders can be placed", "Schedule")
.SetOptimize(0, 23, 1);
_endHour = Param(nameof(EndHour), 24)
.SetDisplay("End Hour", "Hour (UTC) when orders stop", "Schedule")
.SetOptimize(1, 24, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(3).TimeFrame())
.SetDisplay("Candle Type", "Working candle timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null;
_lowest = null;
_entryPrice = 0m;
_pipSize = 0m;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
_highest = new Highest { Length = RangeLength };
_lowest = new Lowest { Length = RangeLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_highest, _lowest, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _highest);
DrawIndicator(area, _lowest);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished)
return;
// Check protective levels
CheckProtection(candle);
if (!_highest.IsFormed || !_lowest.IsFormed)
return;
if (!IsWithinTradingWindow(candle.OpenTime))
return;
if (OrderVolume <= 0m || highest <= lowest)
return;
// Track extremes for trailing
if (Position > 0 && candle.HighPrice > _highestSinceEntry)
_highestSinceEntry = candle.HighPrice;
if (Position < 0 && (_lowestSinceEntry == 0 || candle.LowPrice < _lowestSinceEntry))
_lowestSinceEntry = candle.LowPrice;
// Breakout entry logic
if (Position == 0)
{
if (candle.HighPrice >= highest)
{
BuyMarket(OrderVolume);
}
else if (candle.LowPrice <= lowest)
{
SellMarket(OrderVolume);
}
}
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null) return;
if (Position != 0m && _entryPrice == 0m)
{
_entryPrice = trade.Trade.Price;
_highestSinceEntry = trade.Trade.Price;
_lowestSinceEntry = trade.Trade.Price;
}
if (Position == 0m)
{
_entryPrice = 0m;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
}
}
private void CheckProtection(ICandleMessage candle)
{
if (Position == 0 || _entryPrice == 0m)
return;
var stopDist = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeDist = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
var trailDist = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;
var activation = (TrailingStopPips + TrailingStepPips) * _pipSize;
if (Position > 0)
{
// Stop loss
if (stopDist > 0m && candle.LowPrice <= _entryPrice - stopDist)
{
SellMarket(Math.Abs(Position));
return;
}
// Take profit
if (takeDist > 0m && candle.HighPrice >= _entryPrice + takeDist)
{
SellMarket(Math.Abs(Position));
return;
}
// Trailing stop
if (trailDist > 0m && _highestSinceEntry - _entryPrice > activation)
{
var trailStop = _highestSinceEntry - trailDist;
if (candle.LowPrice <= trailStop)
{
SellMarket(Math.Abs(Position));
return;
}
}
}
else if (Position < 0)
{
if (stopDist > 0m && candle.HighPrice >= _entryPrice + stopDist)
{
BuyMarket(Math.Abs(Position));
return;
}
if (takeDist > 0m && candle.LowPrice <= _entryPrice - takeDist)
{
BuyMarket(Math.Abs(Position));
return;
}
if (trailDist > 0m && _lowestSinceEntry > 0m && _entryPrice - _lowestSinceEntry > activation)
{
var trailStop = _lowestSinceEntry + trailDist;
if (candle.HighPrice >= trailStop)
{
BuyMarket(Math.Abs(Position));
return;
}
}
}
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
return step;
}
private bool IsWithinTradingWindow(DateTimeOffset time)
{
return time.Hour >= StartHour && time.Hour < EndHour;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class hans123_trader_strategy(Strategy):
def __init__(self):
super(hans123_trader_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1)
self._range_length = self.Param("RangeLength", 40)
self._stop_loss_pips = self.Param("StopLossPips", 50)
self._take_profit_pips = self.Param("TakeProfitPips", 50)
self._trailing_stop_pips = self.Param("TrailingStopPips", 10)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._start_hour = self.Param("StartHour", 0)
self._end_hour = self.Param("EndHour", 24)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(3)))
self._highest = None
self._lowest = None
self._entry_price = 0.0
self._pip_size = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def RangeLength(self):
return self._range_length.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(hans123_trader_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._highest = Highest()
self._highest.Length = self.RangeLength
self._lowest = Lowest()
self._lowest.Length = self.RangeLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._highest, self._lowest, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._highest)
self.DrawIndicator(area, self._lowest)
self.DrawOwnTrades(area)
def OnOwnTradeReceived(self, trade):
super(hans123_trader_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Trade is None:
return
pos = float(self.Position)
if pos != 0 and self._entry_price == 0.0:
self._entry_price = float(trade.Trade.Price)
self._highest_since_entry = float(trade.Trade.Price)
self._lowest_since_entry = float(trade.Trade.Price)
if pos == 0:
self._entry_price = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
def _process_candle(self, candle, highest, lowest):
if candle.State != CandleStates.Finished:
return
self._check_protection(candle)
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
if not self._is_within_trading_window(candle.OpenTime):
return
highest_val = float(highest)
lowest_val = float(lowest)
if float(self.OrderVolume) <= 0 or highest_val <= lowest_val:
return
pos = float(self.Position)
if pos > 0 and float(candle.HighPrice) > self._highest_since_entry:
self._highest_since_entry = float(candle.HighPrice)
if pos < 0 and (self._lowest_since_entry == 0 or float(candle.LowPrice) < self._lowest_since_entry):
self._lowest_since_entry = float(candle.LowPrice)
if float(self.Position) == 0:
self._entry_price = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
close = float(candle.ClosePrice)
if float(candle.HighPrice) >= highest_val:
self.BuyMarket(float(self.OrderVolume))
self._entry_price = close
self._highest_since_entry = close
self._lowest_since_entry = close
elif float(candle.LowPrice) <= lowest_val:
self.SellMarket(float(self.OrderVolume))
self._entry_price = close
self._highest_since_entry = close
self._lowest_since_entry = close
def _check_protection(self, candle):
pos = float(self.Position)
if pos == 0 or self._entry_price == 0.0:
return
stop_dist = self.StopLossPips * self._pip_size if self.StopLossPips > 0 else 0.0
take_dist = self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else 0.0
trail_dist = self.TrailingStopPips * self._pip_size if self.TrailingStopPips > 0 else 0.0
activation = (self.TrailingStopPips + self.TrailingStepPips) * self._pip_size
if pos > 0:
if stop_dist > 0 and float(candle.LowPrice) <= self._entry_price - stop_dist:
self.SellMarket(abs(pos))
return
if take_dist > 0 and float(candle.HighPrice) >= self._entry_price + take_dist:
self.SellMarket(abs(pos))
return
if trail_dist > 0 and self._highest_since_entry - self._entry_price > activation:
trail_stop = self._highest_since_entry - trail_dist
if float(candle.LowPrice) <= trail_stop:
self.SellMarket(abs(pos))
return
elif pos < 0:
if stop_dist > 0 and float(candle.HighPrice) >= self._entry_price + stop_dist:
self.BuyMarket(abs(pos))
return
if take_dist > 0 and float(candle.LowPrice) <= self._entry_price - take_dist:
self.BuyMarket(abs(pos))
return
if trail_dist > 0 and self._lowest_since_entry > 0 and self._entry_price - self._lowest_since_entry > activation:
trail_stop = self._lowest_since_entry + trail_dist
if float(candle.HighPrice) >= trail_stop:
self.BuyMarket(abs(pos))
return
def _calculate_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
return step
def _is_within_trading_window(self, time):
return time.Hour >= self.StartHour and time.Hour < self.EndHour
def OnReseted(self):
super(hans123_trader_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._entry_price = 0.0
self._pip_size = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
def CreateClone(self):
return hans123_trader_strategy()