Brakeout Trader v1
Brakeout Trader v1 是一套基于固定价格水平的突破策略。策略仅监控已经收盘的蜡烛,如果最新收盘价穿越用户设定的突破价位,就建立仓位。收盘价向上突破并且允许做多时开多单,收盘价向下突破并且允许做空时开空单。仓位规模根据风险百分比和止损距离计算,可随着账户权益自动调整。
交易逻辑
- 仅处理所选
CandleType的完结蜡烛,未完结的蜡烛会被忽略。 - 保存上一根收盘价以判断是否突破
BreakoutLevel。 - 开多条件:最新收盘价高于
BreakoutLevel,上一根收盘价在该水平或以下,且EnableLong为真。若存在空头仓位,会先平仓后再下多单。 - 开空条件:最新收盘价低于
BreakoutLevel,上一根收盘价在该水平或以上,且EnableShort为真。若存在多头仓位,会先平仓后再下空单。 - 订单按市价提交。数量计算方式确保从入场价到止损价的潜在损失约等于
RiskPercent× 当前账户权益;若无法得到风险仓位,则回退到基础Volume。 - 入场后会记录固定的止损和止盈价位(
StopLossPoints与TakeProfitPoints以 pip 点表示)。价格触及任意价位时立即市价平仓并重置缓存。 - 策略使用净头寸管理,不会同时持有同方向的多笔仓位。
仓位管理
- 多头的止损设在入场价下方,空头的止损设在入场价上方。距离为
StopLossPoints * pip,其中 pip 按Security.PriceStep推导,对于价格保留 3 或 5 位小数的品种会乘以 10,与原始 MQL 逻辑一致。 - 止盈以
TakeProfitPoints对称设置。 - 当同一根蜡烛内既可能触发止损又可能触发止盈时,优先检查止损,以模拟服务器端的保守执行顺序。
- 反向信号始终在建新仓之前平掉当前仓位,避免出现对冲头寸。
- 当仓位归零后,缓存的入场价、止损价和止盈价会被自动清空。
参数说明
BreakoutLevel– 监控突破的固定价格水平。EnableLong/EnableShort– 控制是否允许开多/开空。StopLossPoints– 止损距离(pip 点数)。TakeProfitPoints– 止盈距离(pip 点数)。RiskPercent– 单笔交易允许承担的账户权益百分比,用于按止损距离计算下单数量。CandleType– 用于信号计算的蜡烛类型(默认 15 分钟)。Volume– 在无法按风险计算时使用的基础下单数量。
细节
- 进场条件:最新收盘价向上或向下穿越
BreakoutLevel。 - 多空方向:可双向交易,通过
EnableLong与EnableShort控制。 - 离场条件:达到固定止损/止盈或出现反向突破信号。
- 止损类型:固定距离止损,以 pip 点计量。
- 默认值:
BreakoutLevel = 0、StopLossPoints = 140、TakeProfitPoints = 180、RiskPercent = 10、CandleType = 15 分钟、EnableLong = EnableShort = true。 - 过滤器:除方向开关外,无其他过滤条件。
使用提示
- 请选择 pip 计算方式正确的交易品种;若报价保留 3 或 5 位小数,策略会自动将 pip 乘以 10。
- 需确保账户组合能够提供
CurrentValue,否则下单数量会退回到基础Volume。 - 市价单的成交价可能与蜡烛收盘价不同,必要时可适当调整止损和止盈距离以覆盖滑点。
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 trades when the closing price crosses a predefined level.
/// The strategy sizes positions based on the selected risk percentage and applies static stop-loss and take-profit levels.
/// </summary>
public class BrakeoutTraderV1Strategy : Strategy
{
private readonly StrategyParam<decimal> _breakoutLevel;
private readonly StrategyParam<bool> _enableLong;
private readonly StrategyParam<bool> _enableShort;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousClose;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _pipSize;
/// <summary>
/// Price level that must be broken to generate signals.
/// </summary>
public decimal BreakoutLevel
{
get => _breakoutLevel.Value;
set => _breakoutLevel.Value = value;
}
/// <summary>
/// Enables or disables long breakout trades.
/// </summary>
public bool EnableLong
{
get => _enableLong.Value;
set => _enableLong.Value = value;
}
/// <summary>
/// Enables or disables short breakout trades.
/// </summary>
public bool EnableShort
{
get => _enableShort.Value;
set => _enableShort.Value = value;
}
/// <summary>
/// Stop-loss distance in points relative to the pip size.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in points relative to the pip size.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Percentage of account equity to risk on each trade.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Candle type used to evaluate breakouts.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BrakeoutTraderV1Strategy"/>.
/// </summary>
public BrakeoutTraderV1Strategy()
{
_breakoutLevel = Param(nameof(BreakoutLevel), 65000m)
.SetDisplay("Breakout Level", "Static price level monitored for breakouts", "Signal");
_enableLong = Param(nameof(EnableLong), true)
.SetDisplay("Enable Long", "Allow long breakout positions", "Signal");
_enableShort = Param(nameof(EnableShort), true)
.SetDisplay("Enable Short", "Allow short breakout positions", "Signal");
_stopLossPoints = Param(nameof(StopLossPoints), 140m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss Points", "Stop-loss distance expressed in pip points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 180m)
.SetGreaterThanZero()
.SetDisplay("Take Profit Points", "Take-profit distance expressed in pip points", "Risk");
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Percentage of equity risked per trade", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for breakout detection", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return new[] { (Security, CandleType) };
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousClose = null;
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var priceStep = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals;
_pipSize = priceStep;
if (decimals is 3 or 5)
_pipSize *= 10m;
if (_pipSize <= 0m)
_pipSize = 1m;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
// removed StartProtection(null, null)
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (ManageOpenPosition(candle))
{
UpdatePreviousClose(candle.ClosePrice);
return;
}
var prevClose = _previousClose;
if (prevClose is null)
{
_previousClose = candle.ClosePrice;
return;
}
var currentClose = candle.ClosePrice;
var prevValue = prevClose.Value;
var breakoutUp = currentClose > BreakoutLevel && prevValue <= BreakoutLevel;
var breakoutDown = currentClose < BreakoutLevel && prevValue >= BreakoutLevel;
if (breakoutUp && EnableLong)
{
EnterLong(currentClose);
}
else if (breakoutDown && EnableShort)
{
EnterShort(currentClose);
}
_previousClose = currentClose;
}
private bool ManageOpenPosition(ICandleMessage candle)
{
if (Position > 0)
{
// Exit long if stop-loss is touched.
if (_stopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket();
ResetPositionState();
return true;
}
// Exit long if take-profit is reached.
if (_takePrice is decimal take && candle.HighPrice >= take)
{
SellMarket();
ResetPositionState();
return true;
}
}
else if (Position < 0)
{
// Exit short if stop-loss is touched.
if (_stopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket();
ResetPositionState();
return true;
}
// Exit short if take-profit is reached.
if (_takePrice is decimal take && candle.LowPrice <= take)
{
BuyMarket();
ResetPositionState();
return true;
}
}
else if (_entryPrice.HasValue)
{
// Reset cached levels after position is fully closed.
ResetPositionState();
}
return false;
}
private void EnterLong(decimal price)
{
if (Position > 0)
return;
var volume = CalculateOrderVolume();
var closingVolume = Position < 0 ? Math.Abs(Position) : 0m;
var totalVolume = closingVolume + volume;
if (totalVolume <= 0m)
return;
if (closingVolume > 0m)
ResetPositionState();
BuyMarket();
SetPositionTargets(price, true, volume > 0m);
}
private void EnterShort(decimal price)
{
if (Position < 0)
return;
var volume = CalculateOrderVolume();
var closingVolume = Position > 0 ? Position : 0m;
var totalVolume = closingVolume + volume;
if (totalVolume <= 0m)
return;
if (closingVolume > 0m)
ResetPositionState();
SellMarket();
SetPositionTargets(price, false, volume > 0m);
}
private void SetPositionTargets(decimal entryPrice, bool isLong, bool hasNewPosition)
{
if (!hasNewPosition)
{
return;
}
_entryPrice = entryPrice;
if (StopLossPoints > 0m && _pipSize > 0m)
_stopPrice = isLong
? entryPrice - StopLossPoints * _pipSize
: entryPrice + StopLossPoints * _pipSize;
else
_stopPrice = null;
if (TakeProfitPoints > 0m && _pipSize > 0m)
_takePrice = isLong
? entryPrice + TakeProfitPoints * _pipSize
: entryPrice - TakeProfitPoints * _pipSize;
else
_takePrice = null;
}
private decimal CalculateOrderVolume()
{
var baseVolume = Volume;
var stopDistance = StopLossPoints * _pipSize;
if (stopDistance <= 0m || RiskPercent <= 0m)
return AdjustVolume(baseVolume);
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return AdjustVolume(baseVolume);
var riskValue = equity * RiskPercent / 100m;
if (riskValue <= 0m)
return AdjustVolume(baseVolume);
var qty = riskValue / stopDistance;
var adjusted = AdjustVolume(qty);
return adjusted > 0m ? adjusted : AdjustVolume(baseVolume);
}
private decimal AdjustVolume(decimal volume)
{
var security = Security;
if (security != null)
{
var step = security.VolumeStep;
if (step is decimal s && s > 0m)
{
volume = Math.Floor(volume / s) * s;
}
var min = security.MinVolume;
if (min is decimal minVol && volume < minVol)
volume = minVol;
var max = security.MaxVolume;
if (max is decimal maxVol && maxVol > 0m && volume > maxVol)
volume = maxVol;
if (volume <= 0m)
volume = step is decimal stepVal && stepVal > 0m ? stepVal : 0m;
}
if (volume <= 0m)
volume = volume == 0m ? 1m : Math.Abs(volume);
return volume;
}
private void UpdatePreviousClose(decimal close)
{
_previousClose = close;
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = 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, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class brakeout_trader_v1_strategy(Strategy):
def __init__(self):
super(brakeout_trader_v1_strategy, self).__init__()
self._breakout_level = self.Param("BreakoutLevel", 65000.0)
self._enable_long = self.Param("EnableLong", True)
self._enable_short = self.Param("EnableShort", True)
self._stop_loss_points = self.Param("StopLossPoints", 140.0)
self._take_profit_points = self.Param("TakeProfitPoints", 180.0)
self._risk_percent = self.Param("RiskPercent", 10.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_close = None
self._entry_price = None
self._stop_price = None
self._take_price = None
self._pip_size = 0.0
@property
def BreakoutLevel(self):
return self._breakout_level.Value
@BreakoutLevel.setter
def BreakoutLevel(self, value):
self._breakout_level.Value = value
@property
def EnableLong(self):
return self._enable_long.Value
@EnableLong.setter
def EnableLong(self, value):
self._enable_long.Value = value
@property
def EnableShort(self):
return self._enable_short.Value
@EnableShort.setter
def EnableShort(self, value):
self._enable_short.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_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 RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.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(brakeout_trader_v1_strategy, self).OnStarted2(time)
ps = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
self._pip_size = ps
if self.Security is not None and self.Security.Decimals is not None:
d = self.Security.Decimals
if d == 3 or d == 5:
self._pip_size = ps * 10.0
if self._pip_size <= 0.0:
self._pip_size = 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._manage_open_position(candle):
self._previous_close = float(candle.ClosePrice)
return
if self._previous_close is None:
self._previous_close = float(candle.ClosePrice)
return
current_close = float(candle.ClosePrice)
prev_value = self._previous_close
level = float(self.BreakoutLevel)
breakout_up = current_close > level and prev_value <= level
breakout_down = current_close < level and prev_value >= level
if breakout_up and self.EnableLong:
self._enter_long(current_close)
elif breakout_down and self.EnableShort:
self._enter_short(current_close)
self._previous_close = current_close
def _manage_open_position(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_position_state()
return True
if self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset_position_state()
return True
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_position_state()
return True
if self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset_position_state()
return True
elif self._entry_price is not None:
self._reset_position_state()
return False
def _enter_long(self, price):
if self.Position > 0:
return
if self.Position < 0:
self._reset_position_state()
self.BuyMarket()
self._set_position_targets(price, True)
def _enter_short(self, price):
if self.Position < 0:
return
if self.Position > 0:
self._reset_position_state()
self.SellMarket()
self._set_position_targets(price, False)
def _set_position_targets(self, entry_price, is_long):
self._entry_price = entry_price
sl = float(self.StopLossPoints)
tp = float(self.TakeProfitPoints)
if sl > 0.0 and self._pip_size > 0.0:
self._stop_price = entry_price - sl * self._pip_size if is_long else entry_price + sl * self._pip_size
else:
self._stop_price = None
if tp > 0.0 and self._pip_size > 0.0:
self._take_price = entry_price + tp * self._pip_size if is_long else entry_price - tp * self._pip_size
else:
self._take_price = None
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(brakeout_trader_v1_strategy, self).OnReseted()
self._previous_close = None
self._entry_price = None
self._stop_price = None
self._take_price = None
self._pip_size = 0.0
def CreateClone(self):
return brakeout_trader_v1_strategy()