BHS System 策略
概述
BHS System 是将原始 MetaTrader 5 专家顾问移植到 StockSharp 高级 API 的突破型策略。策略跟踪收盘价与 Kaufman 自适应移动平均线(AMA)之间的关系:当收盘价高于 AMA 时,为潜在的多头突破做准备;当收盘价低于 AMA 时,为潜在的空头扩张做准备。算法不会立刻进场,而是等待价格触及用户指定的“整数关口”,并在这些价位挂出止损单,从而完全复刻 MQL 版本始终在圆整价位挂单的行为。
交易逻辑
- 每根完成的 K 线都会根据设置的点数步长和合约的最小价格变动,计算出最近的上方和下方圆整价位。
- 将上一根 K 线的 AMA 数值(对应原始 MQL 中的
iAMAGet(1))与当前 K 线的收盘价进行比较。 - 若没有持仓且没有挂出的入场单:
- 当收盘价大于 AMA 时,在圆整价位的上方挂出买入止损单;
- 当收盘价小于 AMA 时,在圆整价位的下方挂出卖出止损单。
- 入场挂单会按照设置的小时数自动失效,对应 MT5 中的挂单有效期。
- 当挂单成交后,会取消相反方向的挂单,并按设定的止损距离注册保护性止损单,同时开始跟踪止损逻辑。
- 只有当价格向有利方向移动超过“跟踪距离 + 跟踪步长”时,才会移动止损,以避免频繁修改订单并忠实还原 MQL 版本的离散式跟踪机制。
风险控制
- 初始止损: 多空独立的点数距离会换算成绝对价格偏移,在入场后立即生成保护性止损单。
- 跟踪止损: 多单与空单拥有独立的跟踪距离,只有当新的止损价相对旧价至少改善一个跟踪步长时才会移动,防止在震荡环境中过度调整。
- 挂单失效: 入场挂单会记录创建时间,当挂单存续时间超过设定值时自动撤单,避免长时间暴露。
参数
OrderVolume– 入场与保护性订单使用的手数。StopLossBuyPoints/StopLossSellPoints– 多头与空头的初始止损点数。TrailingStopBuyPoints/TrailingStopSellPoints– 多头与空头的跟踪止损点数。TrailingStepPoints– 每次更新跟踪止损所需的额外点数间隔。RoundStepPoints– 用于计算圆整价位的点数步长。ExpirationHours– 入场挂单的有效期(小时),为 0 时表示不过期。AmaLength、AmaFastPeriod、AmaSlowPeriod– Kaufman AMA 指标的参数。CandleType– 驱动策略的 K 线数据类型或周期。
实现说明
- 策略使用 StockSharp 自带的
KaufmanAdaptiveMovingAverage指标,并采用文件级命名空间以符合仓库规范。 - 所有交易动作均通过高级 API(
BuyStop、SellStop、CancelOrder等)完成,不调用任何GetValue方法读取指标值。 - 若存在图表上下文,策略会绘制价格、AMA 曲线以及自身成交记录,方便可视化分析。
- 保护性止损与跟踪止损共用同一个订单引用,从而在更新时直接修改原有止损单,避免产生多余订单。
- 代码中的注释全部使用英文,并保持与原始 MQL 版本一致的跟踪触发条件,确保移植后的行为一致。
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>
/// Breakout strategy that places stop orders on rounded price levels guided by a Kaufman adaptive moving average.
/// </summary>
public class BhsSystemStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _stopLossBuyPoints;
private readonly StrategyParam<int> _stopLossSellPoints;
private readonly StrategyParam<int> _trailingStopBuyPoints;
private readonly StrategyParam<int> _trailingStopSellPoints;
private readonly StrategyParam<int> _trailingStepPoints;
private readonly StrategyParam<int> _roundStepPoints;
private readonly StrategyParam<decimal> _expirationHours;
private readonly StrategyParam<int> _amaLength;
private readonly StrategyParam<int> _amaFastPeriod;
private readonly StrategyParam<int> _amaSlowPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _previousAma;
private bool _hasPreviousAma;
private decimal? _buyStopLevel;
private decimal? _sellStopLevel;
private DateTimeOffset? _buyOrderTime;
private DateTimeOffset? _sellOrderTime;
private decimal _entryPrice;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
/// <summary>
/// Trade volume used for both entry and protective orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Stop-loss distance for long trades expressed in points.
/// </summary>
public int StopLossBuyPoints
{
get => _stopLossBuyPoints.Value;
set => _stopLossBuyPoints.Value = value;
}
/// <summary>
/// Stop-loss distance for short trades expressed in points.
/// </summary>
public int StopLossSellPoints
{
get => _stopLossSellPoints.Value;
set => _stopLossSellPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in points for long positions.
/// </summary>
public int TrailingStopBuyPoints
{
get => _trailingStopBuyPoints.Value;
set => _trailingStopBuyPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in points for short positions.
/// </summary>
public int TrailingStopSellPoints
{
get => _trailingStopSellPoints.Value;
set => _trailingStopSellPoints.Value = value;
}
/// <summary>
/// Minimum step in points between trailing stop updates.
/// </summary>
public int TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Step used to build rounded trigger prices in points.
/// </summary>
public int RoundStepPoints
{
get => _roundStepPoints.Value;
set => _roundStepPoints.Value = value;
}
/// <summary>
/// Lifetime of pending entry orders in hours.
/// </summary>
public decimal ExpirationHours
{
get => _expirationHours.Value;
set => _expirationHours.Value = value;
}
/// <summary>
/// Main period of the adaptive moving average.
/// </summary>
public int AmaLength
{
get => _amaLength.Value;
set => _amaLength.Value = value;
}
/// <summary>
/// Fast smoothing constant of the adaptive moving average.
/// </summary>
public int AmaFastPeriod
{
get => _amaFastPeriod.Value;
set => _amaFastPeriod.Value = value;
}
/// <summary>
/// Slow smoothing constant of the adaptive moving average.
/// </summary>
public int AmaSlowPeriod
{
get => _amaSlowPeriod.Value;
set => _amaSlowPeriod.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="BhsSystemStrategy"/> class.
/// </summary>
public BhsSystemStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Lot size used for entry orders", "Trading");
_stopLossBuyPoints = Param(nameof(StopLossBuyPoints), 300)
.SetNotNegative()
.SetDisplay("Stop Loss Buy (points)", "Distance in points for long stop loss", "Risk");
_stopLossSellPoints = Param(nameof(StopLossSellPoints), 300)
.SetNotNegative()
.SetDisplay("Stop Loss Sell (points)", "Distance in points for short stop loss", "Risk");
_trailingStopBuyPoints = Param(nameof(TrailingStopBuyPoints), 100)
.SetNotNegative()
.SetDisplay("Trailing Stop Buy (points)", "Trailing distance in points for long positions", "Risk");
_trailingStopSellPoints = Param(nameof(TrailingStopSellPoints), 100)
.SetNotNegative()
.SetDisplay("Trailing Stop Sell (points)", "Trailing distance in points for short positions", "Risk");
_trailingStepPoints = Param(nameof(TrailingStepPoints), 10)
.SetNotNegative()
.SetDisplay("Trailing Step (points)", "Minimum step in points between trailing updates", "Risk");
_roundStepPoints = Param(nameof(RoundStepPoints), 2000)
.SetGreaterThanZero()
.SetDisplay("Round Step (points)", "Number of points used to build round price levels", "Execution");
_expirationHours = Param(nameof(ExpirationHours), 1m)
.SetNotNegative()
.SetDisplay("Order Expiration (hours)", "Lifetime of pending entry orders in hours", "Execution");
_amaLength = Param(nameof(AmaLength), 15)
.SetGreaterThanZero()
.SetDisplay("AMA Length", "Adaptive moving average period", "Indicators");
_amaFastPeriod = Param(nameof(AmaFastPeriod), 2)
.SetGreaterThanZero()
.SetDisplay("AMA Fast Period", "Fast smoothing constant for AMA", "Indicators");
_amaSlowPeriod = Param(nameof(AmaSlowPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("AMA Slow Period", "Slow smoothing constant for AMA", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousAma = 0m;
_hasPreviousAma = false;
_buyStopLevel = null;
_sellStopLevel = null;
_buyOrderTime = null;
_sellOrderTime = null;
_entryPrice = 0m;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Configure the adaptive moving average with user parameters.
var ama = new KaufmanAdaptiveMovingAverage
{
Length = AmaLength,
FastSCPeriod = AmaFastPeriod,
SlowSCPeriod = AmaSlowPeriod
};
// Subscribe to candle data and bind indicator updates.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ama, ProcessCandle)
.Start();
// Draw price, indicator and trades if a chart is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ama);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal amaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_hasPreviousAma)
{
_previousAma = amaValue;
_hasPreviousAma = true;
return;
}
// Check protective stops
CheckStopLoss(candle);
CheckTrailingStop(candle);
// Expire old pending levels
CancelExpiredLevels();
// Check if pending levels are triggered
CheckPendingTriggers(candle);
var price = candle.ClosePrice;
var (_, priceCeil, priceFloor) = CalculateRoundLevels(price);
// Track extremes for trailing
if (Position > 0 && price > _highestSinceEntry)
_highestSinceEntry = price;
if (Position < 0 && (_lowestSinceEntry == 0 || price < _lowestSinceEntry))
_lowestSinceEntry = price;
var hasPendingLevels = _buyStopLevel.HasValue || _sellStopLevel.HasValue;
if (Position == 0 && !hasPendingLevels)
{
if (price > _previousAma)
{
_buyStopLevel = priceCeil;
_buyOrderTime = candle.OpenTime;
}
else if (price < _previousAma)
{
_sellStopLevel = priceFloor;
_sellOrderTime = candle.OpenTime;
}
}
_previousAma = amaValue;
}
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 CheckStopLoss(ICandleMessage candle)
{
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && StopLossBuyPoints > 0)
{
var stopPrice = _entryPrice - StopLossBuyPoints * step;
if (candle.LowPrice <= stopPrice)
{
SellMarket(Math.Abs(Position));
return;
}
}
if (Position < 0 && StopLossSellPoints > 0)
{
var stopPrice = _entryPrice + StopLossSellPoints * step;
if (candle.HighPrice >= stopPrice)
{
BuyMarket(Math.Abs(Position));
}
}
}
private void CheckTrailingStop(ICandleMessage candle)
{
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && TrailingStopBuyPoints > 0)
{
var trailingDist = TrailingStopBuyPoints * step;
var trailingStep = TrailingStepPoints * step;
var profit = _highestSinceEntry - _entryPrice;
if (profit > trailingDist + trailingStep)
{
var trailStop = _highestSinceEntry - trailingDist;
if (candle.LowPrice <= trailStop)
{
SellMarket(Math.Abs(Position));
return;
}
}
}
if (Position < 0 && TrailingStopSellPoints > 0 && _lowestSinceEntry > 0)
{
var trailingDist = TrailingStopSellPoints * step;
var trailingStep = TrailingStepPoints * step;
var profit = _entryPrice - _lowestSinceEntry;
if (profit > trailingDist + trailingStep)
{
var trailStop = _lowestSinceEntry + trailingDist;
if (candle.HighPrice >= trailStop)
{
BuyMarket(Math.Abs(Position));
}
}
}
}
private void CheckPendingTriggers(ICandleMessage candle)
{
if (_buyStopLevel.HasValue && Position <= 0 && candle.HighPrice >= _buyStopLevel.Value)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(OrderVolume);
_buyStopLevel = null;
_buyOrderTime = null;
_sellStopLevel = null;
_sellOrderTime = null;
}
if (_sellStopLevel.HasValue && Position >= 0 && candle.LowPrice <= _sellStopLevel.Value)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(OrderVolume);
_sellStopLevel = null;
_sellOrderTime = null;
_buyStopLevel = null;
_buyOrderTime = null;
}
}
private void CancelExpiredLevels()
{
if (ExpirationHours <= 0m)
return;
var expiration = TimeSpan.FromHours((double)ExpirationHours);
var now = CurrentTime;
if (_buyOrderTime.HasValue && now - _buyOrderTime.Value >= expiration)
{
_buyStopLevel = null;
_buyOrderTime = null;
}
if (_sellOrderTime.HasValue && now - _sellOrderTime.Value >= expiration)
{
_sellStopLevel = null;
_sellOrderTime = null;
}
}
private (decimal rounded, decimal ceil, decimal floor) CalculateRoundLevels(decimal price)
{
var point = Security?.PriceStep ?? 1m;
var stepPoints = RoundStepPoints;
if (point <= 0m || stepPoints <= 0)
return (price, price, price);
var step = stepPoints * point;
if (step <= 0m)
return (price, price, price);
var ratio = price / step;
var roundedIndex = decimal.Round(ratio, 0, MidpointRounding.AwayFromZero);
var priceRound = roundedIndex * step;
var ceilIndex = decimal.Ceiling((priceRound + step / 2m) / step);
var floorIndex = decimal.Floor((priceRound - step / 2m) / step);
var priceCeil = ceilIndex * step;
var priceFloor = floorIndex * step;
return (priceRound, priceCeil, priceFloor);
}
}
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 KaufmanAdaptiveMovingAverage
from StockSharp.Algo.Strategies import Strategy
class bhs_system_strategy(Strategy):
def __init__(self):
super(bhs_system_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1)
self._stop_loss_buy_points = self.Param("StopLossBuyPoints", 300)
self._stop_loss_sell_points = self.Param("StopLossSellPoints", 300)
self._trailing_stop_buy_points = self.Param("TrailingStopBuyPoints", 100)
self._trailing_stop_sell_points = self.Param("TrailingStopSellPoints", 100)
self._trailing_step_points = self.Param("TrailingStepPoints", 10)
self._round_step_points = self.Param("RoundStepPoints", 2000)
self._expiration_hours = self.Param("ExpirationHours", 1.0)
self._ama_length = self.Param("AmaLength", 15)
self._ama_fast_period = self.Param("AmaFastPeriod", 2)
self._ama_slow_period = self.Param("AmaSlowPeriod", 30)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_ama = 0.0
self._has_previous_ama = False
self._buy_stop_level = None
self._sell_stop_level = None
self._buy_order_time = None
self._sell_order_time = None
self._entry_price = 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 StopLossBuyPoints(self):
return self._stop_loss_buy_points.Value
@property
def StopLossSellPoints(self):
return self._stop_loss_sell_points.Value
@property
def TrailingStopBuyPoints(self):
return self._trailing_stop_buy_points.Value
@property
def TrailingStopSellPoints(self):
return self._trailing_stop_sell_points.Value
@property
def TrailingStepPoints(self):
return self._trailing_step_points.Value
@property
def RoundStepPoints(self):
return self._round_step_points.Value
@property
def ExpirationHours(self):
return self._expiration_hours.Value
@property
def AmaLength(self):
return self._ama_length.Value
@property
def AmaFastPeriod(self):
return self._ama_fast_period.Value
@property
def AmaSlowPeriod(self):
return self._ama_slow_period.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(bhs_system_strategy, self).OnStarted2(time)
ama = KaufmanAdaptiveMovingAverage()
ama.Length = self.AmaLength
ama.FastSCPeriod = self.AmaFastPeriod
ama.SlowSCPeriod = self.AmaSlowPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ama, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ama)
self.DrawOwnTrades(area)
def OnOwnTradeReceived(self, trade):
super(bhs_system_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, ama_value):
if candle.State != CandleStates.Finished:
return
ama_val = float(ama_value)
if not self._has_previous_ama:
self._previous_ama = ama_val
self._has_previous_ama = True
return
self._check_stop_loss(candle)
self._check_trailing_stop(candle)
self._cancel_expired_levels()
self._check_pending_triggers(candle)
price = float(candle.ClosePrice)
price_ceil, price_floor = self._calculate_round_levels(price)
pos = float(self.Position)
if pos > 0 and price > self._highest_since_entry:
self._highest_since_entry = price
if pos < 0 and (self._lowest_since_entry == 0 or price < self._lowest_since_entry):
self._lowest_since_entry = price
has_pending = self._buy_stop_level is not None or self._sell_stop_level is not None
if float(self.Position) == 0 and not has_pending:
if price > self._previous_ama:
self._buy_stop_level = price_ceil
self._buy_order_time = candle.OpenTime
elif price < self._previous_ama:
self._sell_stop_level = price_floor
self._sell_order_time = candle.OpenTime
self._previous_ama = ama_val
def _check_stop_loss(self, candle):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
pos = float(self.Position)
if pos > 0 and self.StopLossBuyPoints > 0:
stop_price = self._entry_price - self.StopLossBuyPoints * step
if float(candle.LowPrice) <= stop_price:
self.SellMarket(abs(pos))
return
if pos < 0 and self.StopLossSellPoints > 0:
stop_price = self._entry_price + self.StopLossSellPoints * step
if float(candle.HighPrice) >= stop_price:
self.BuyMarket(abs(pos))
def _check_trailing_stop(self, candle):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
pos = float(self.Position)
if pos > 0 and self.TrailingStopBuyPoints > 0:
trailing_dist = self.TrailingStopBuyPoints * step
trailing_step = self.TrailingStepPoints * step
profit = self._highest_since_entry - self._entry_price
if profit > trailing_dist + trailing_step:
trail_stop = self._highest_since_entry - trailing_dist
if float(candle.LowPrice) <= trail_stop:
self.SellMarket(abs(pos))
return
if pos < 0 and self.TrailingStopSellPoints > 0 and self._lowest_since_entry > 0:
trailing_dist = self.TrailingStopSellPoints * step
trailing_step = self.TrailingStepPoints * step
profit = self._entry_price - self._lowest_since_entry
if profit > trailing_dist + trailing_step:
trail_stop = self._lowest_since_entry + trailing_dist
if float(candle.HighPrice) >= trail_stop:
self.BuyMarket(abs(pos))
def _check_pending_triggers(self, candle):
pos = float(self.Position)
if self._buy_stop_level is not None and pos <= 0 and float(candle.HighPrice) >= self._buy_stop_level:
if pos < 0:
self.BuyMarket(abs(pos))
self.BuyMarket(float(self.OrderVolume))
self._buy_stop_level = None
self._buy_order_time = None
self._sell_stop_level = None
self._sell_order_time = None
pos = float(self.Position)
if self._sell_stop_level is not None and pos >= 0 and float(candle.LowPrice) <= self._sell_stop_level:
if pos > 0:
self.SellMarket(abs(pos))
self.SellMarket(float(self.OrderVolume))
self._sell_stop_level = None
self._sell_order_time = None
self._buy_stop_level = None
self._buy_order_time = None
def _cancel_expired_levels(self):
if float(self.ExpirationHours) <= 0:
return
expiration = TimeSpan.FromHours(float(self.ExpirationHours))
now = self.CurrentTime
if self._buy_order_time is not None and now - self._buy_order_time >= expiration:
self._buy_stop_level = None
self._buy_order_time = None
if self._sell_order_time is not None and now - self._sell_order_time >= expiration:
self._sell_stop_level = None
self._sell_order_time = None
def _calculate_round_levels(self, price):
sec = self.Security
point = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
step_points = self.RoundStepPoints
if point <= 0 or step_points <= 0:
return (price, price)
step = step_points * point
if step <= 0:
return (price, price)
ratio = price / step
rounded_index = round(ratio)
price_round = rounded_index * step
import math
ceil_index = math.ceil((price_round + step / 2.0) / step)
floor_index = math.floor((price_round - step / 2.0) / step)
price_ceil = ceil_index * step
price_floor = floor_index * step
return (price_ceil, price_floor)
def OnReseted(self):
super(bhs_system_strategy, self).OnReseted()
self._previous_ama = 0.0
self._has_previous_ama = False
self._buy_stop_level = None
self._sell_stop_level = None
self._buy_order_time = None
self._sell_order_time = None
self._entry_price = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
def CreateClone(self):
return bhs_system_strategy()