周一典型突破策略
概述
周一典型突破策略 是 MetaTrader 专家顾问 yi1ywioff50qr6(仓库编号 8187)的 C# 版本。原始脚本在每根小时 K 线收盘后判断下一根开盘价是否高于上一根的典型价格 (最高 + 最低 + 收盘) / 3,若条件满足且无持仓,则在周一固定时刻开多。本实现使用 StockSharp 高级策略框架复刻该逻辑,并提供详细的参数用于仓位管理与风险控制。
交易逻辑
- 订阅参数指定的 K 线序列(默认 1 小时)。
- 每当一根 K 线结束时执行以下检查:
- 当前 K 线属于周一。
- K 线开盘小时与参数 Open Hour(默认 09:00)相同。
- 当前没有持仓或挂单。
- 开盘价高于上一根 K 线的典型价格。
- 若全部条件成立,则按照资金管理模块计算出的手数市价买入,同时通过
StartProtection设置止损与止盈距离。
策略只做多,每根符合条件的周一 K 线最多触发一次交易。
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
FixedVolume |
固定下单手数;设为 0 则启用权益分级手数表。 |
0.1 |
OpenHour |
用于判定信号的小时数(0-23)。 | 9 |
StopLossPoints |
止损距离(价格点),为 0 时禁用止损。 |
50 |
TakeProfitPoints |
止盈距离(价格点),为 0 时禁用止盈。 |
20 |
InitialEquity |
启动权益分级手数的最低权益阈值。 | 600 |
EquityStep |
每增加多少权益提升一次手数。 | 300 |
InitialStepVolume |
当权益达到阈值时使用的基础手数。 | 0.4 |
VolumeStep |
每达到一个权益阶梯增加的额外手数。 | 0.2 |
CandleType |
用于计算信号的 K 线类型(默认 1 小时)。 | 1 小时时间框架 |
资金管理
- 当
FixedVolume大于零时,策略始终使用该固定手数。 - 当
FixedVolume等于零时,策略按照以下步骤计算仓位:- 若权益低于
InitialEquity,使用合约的最小手数。 - 若权益达到或超过阈值,从
InitialStepVolume开始,并且每超过一个EquityStep增加VolumeStep手。 - 计算结果会根据交易品种的最小/步进手数进行对齐。
- 若权益低于
风险管理
在 OnStarted 中调用 StartProtection,将止损与止盈点数自动转换为基于 PriceStep 的价格距离。将任一参数设为零即可关闭对应保护。
使用提示
- 原脚本针对小时级别设计。若选择更低周期,可能在同一小时内出现多根 K 线;策略仍会在持仓存在或挂单未清的情况下忽略新的信号。
- 若启用动态手数,请确保投资组合的权益数据 (
Portfolio.CurrentValue) 可用。 - 需要订阅
CandleType指定的 K 线数据以及一级行情以便发送市价单。
转换说明
- MQL 中基于魔术号的订单过滤被
Position和ActiveOrders检查取代。 - 时间比较使用蜡烛的
DateTimeOffset并调用.ToLocalTime(),以保持与图表时区一致。 - 止损/止盈由 StockSharp 的
StartProtection高级接口管理,省去手工下单。
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;
using StockSharp.Algo;
using StockSharp.Algo.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the MetaTrader strategy yi1ywioff50qr6 (ID 8187).
/// Buys on Monday when the hourly open breaks above the prior bar's typical price.
/// Applies equity-based position sizing when the fixed lot is disabled.
/// </summary>
public class MondayTypicalBreakoutStrategy : Strategy
{
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _openHour;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _initialEquity;
private readonly StrategyParam<decimal> _equityStep;
private readonly StrategyParam<decimal> _initialStepVolume;
private readonly StrategyParam<decimal> _volumeStep;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _previousCandle;
private DateTimeOffset? _lastSignalTime;
private decimal _priceStep;
/// <summary>
/// Initializes parameters to mirror the MQL expert defaults.
/// </summary>
public MondayTypicalBreakoutStrategy()
{
_fixedVolume = Param(nameof(FixedVolume), 0.1m)
.SetNotNegative()
.SetDisplay("Fixed Volume", "Lot size used for entries (set to 0 to enable equity scaling)", "Risk");
_openHour = Param(nameof(OpenHour), 9)
.SetRange(0, 23)
.SetDisplay("Open Hour", "Hour of the session to evaluate Monday breakout entries", "Session");
_stopLossPoints = Param(nameof(StopLossPoints), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk");
_initialEquity = Param(nameof(InitialEquity), 600m)
.SetGreaterThanZero()
.SetDisplay("Initial Equity", "Account equity threshold that triggers the first scaling tier", "Money Management");
_equityStep = Param(nameof(EquityStep), 300m)
.SetGreaterThanZero()
.SetDisplay("Equity Step", "Incremental equity required to raise the position size", "Money Management");
_initialStepVolume = Param(nameof(InitialStepVolume), 0.4m)
.SetGreaterThanZero()
.SetDisplay("Initial Step Volume", "Lot size used once the equity threshold is met", "Money Management");
_volumeStep = Param(nameof(VolumeStep), 0.2m)
.SetNotNegative()
.SetDisplay("Volume Step", "Additional lot size added for each equity step", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for detecting the Monday breakout", "General");
}
/// <summary>
/// Fixed lot size used for entries (set to zero to enable scaling).
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Hour (0-23) when Monday entries are evaluated.
/// </summary>
public int OpenHour
{
get => _openHour.Value;
set => _openHour.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Minimum equity required before the scaling table becomes active.
/// </summary>
public decimal InitialEquity
{
get => _initialEquity.Value;
set => _initialEquity.Value = value;
}
/// <summary>
/// Equity increment that increases the trade size by <see cref="VolumeStep"/>.
/// </summary>
public decimal EquityStep
{
get => _equityStep.Value;
set => _equityStep.Value = value;
}
/// <summary>
/// Volume applied when the first equity threshold is met.
/// </summary>
public decimal InitialStepVolume
{
get => _initialStepVolume.Value;
set => _initialStepVolume.Value = value;
}
/// <summary>
/// Additional volume added for each equity tier.
/// </summary>
public decimal VolumeStep
{
get => _volumeStep.Value;
set => _volumeStep.Value = value;
}
/// <summary>
/// Candle type used for detecting breakout conditions.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousCandle = null;
_lastSignalTime = null;
_priceStep = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security?.PriceStep ?? 0m;
if (_priceStep <= 0m)
_priceStep = 0.0001m;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
if (TakeProfitPoints > 0 || StopLossPoints > 0)
{
var takeDistance = TakeProfitPoints > 0
? new Unit(TakeProfitPoints * _priceStep, UnitTypes.Absolute)
: new Unit(0m);
var stopDistance = StopLossPoints > 0
? new Unit(StopLossPoints * _priceStep, UnitTypes.Absolute)
: new Unit(0m);
StartProtection(takeProfit: takeDistance, stopLoss: stopDistance);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var previous = _previousCandle;
_previousCandle = candle;
if (previous is null)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position != 0m)
return;
var candleTime = candle.OpenTime.ToLocalTime();
if (candleTime.DayOfWeek != DayOfWeek.Monday)
return;
if (candleTime.Hour != OpenHour)
return;
if (_lastSignalTime is DateTimeOffset last && last == candle.OpenTime)
return;
var typicalPrice = (previous.HighPrice + previous.LowPrice + previous.ClosePrice) / 3m;
if (candle.OpenPrice <= typicalPrice)
return;
var volume = CalculateOrderVolume();
volume = AlignVolume(volume);
if (volume <= 0m)
return;
BuyMarket(volume);
_lastSignalTime = candle.OpenTime;
}
private decimal CalculateOrderVolume()
{
var fixedVolume = FixedVolume;
if (fixedVolume > 0m)
return fixedVolume;
var security = Security;
var portfolio = Portfolio;
var minVolume = security?.MinVolume ?? 0m;
if (minVolume <= 0m)
minVolume = 0.01m;
var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
if (equity <= 0m)
return minVolume;
if (equity < InitialEquity)
return minVolume;
if (EquityStep <= 0m)
return InitialStepVolume;
var stepsDecimal = (equity - InitialEquity) / EquityStep;
if (stepsDecimal < 0m)
stepsDecimal = 0m;
var steps = (int)Math.Floor(stepsDecimal);
var dynamicVolume = InitialStepVolume + VolumeStep * steps;
if (dynamicVolume < minVolume)
dynamicVolume = minVolume;
return dynamicVolume;
}
private decimal AlignVolume(decimal volume)
{
var security = Security;
if (security == null)
return volume;
var minVolume = security.MinVolume ?? 0m;
var maxVolume = security.MaxVolume ?? 0m;
var step = security.VolumeStep ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
if (maxVolume > 0m && volume > maxVolume)
volume = maxVolume;
if (step > 0m)
{
var steps = Math.Round(volume / step, MidpointRounding.AwayFromZero);
volume = steps * step;
}
return volume;
}
}
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, DayOfWeek
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
class monday_typical_breakout_strategy(Strategy):
def __init__(self):
super(monday_typical_breakout_strategy, self).__init__()
self._fixed_volume = self.Param("FixedVolume", 0.1) \
.SetDisplay("Fixed Volume", "Lot size used for entries (set to 0 to enable equity scaling)", "Risk")
self._open_hour = self.Param("OpenHour", 9) \
.SetDisplay("Open Hour", "Hour of the session to evaluate Monday breakout entries", "Session")
self._stop_loss_points = self.Param("StopLossPoints", 50) \
.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 20) \
.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
self._initial_equity = self.Param("InitialEquity", 600.0) \
.SetDisplay("Initial Equity", "Account equity threshold that triggers the first scaling tier", "Money Management")
self._equity_step = self.Param("EquityStep", 300.0) \
.SetDisplay("Equity Step", "Incremental equity required to raise the position size", "Money Management")
self._initial_step_volume = self.Param("InitialStepVolume", 0.4) \
.SetDisplay("Initial Step Volume", "Lot size used once the equity threshold is met", "Money Management")
self._volume_step = self.Param("VolumeStep", 0.2) \
.SetDisplay("Volume Step", "Additional lot size added for each equity step", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for detecting the Monday breakout", "General")
self._previous_candle = None
self._last_signal_time = None
self._price_step = 0.0
@property
def FixedVolume(self):
return self._fixed_volume.Value
@FixedVolume.setter
def FixedVolume(self, value):
self._fixed_volume.Value = value
@property
def OpenHour(self):
return self._open_hour.Value
@OpenHour.setter
def OpenHour(self, value):
self._open_hour.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 InitialEquity(self):
return self._initial_equity.Value
@InitialEquity.setter
def InitialEquity(self, value):
self._initial_equity.Value = value
@property
def EquityStep(self):
return self._equity_step.Value
@EquityStep.setter
def EquityStep(self, value):
self._equity_step.Value = value
@property
def InitialStepVolume(self):
return self._initial_step_volume.Value
@InitialStepVolume.setter
def InitialStepVolume(self, value):
self._initial_step_volume.Value = value
@property
def VolumeStep(self):
return self._volume_step.Value
@VolumeStep.setter
def VolumeStep(self, value):
self._volume_step.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(monday_typical_breakout_strategy, self).OnStarted2(time)
ps = self.Security.PriceStep if self.Security is not None else 0
self._price_step = float(ps) if ps is not None and float(ps) > 0 else 0.0001
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
tp = int(self.TakeProfitPoints)
sl = int(self.StopLossPoints)
if tp > 0 or sl > 0:
take_dist = Unit(tp * self._price_step, UnitTypes.Absolute) if tp > 0 else Unit(0)
stop_dist = Unit(sl * self._price_step, UnitTypes.Absolute) if sl > 0 else Unit(0)
self.StartProtection(takeProfit=take_dist, stopLoss=stop_dist)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
previous = self._previous_candle
self._previous_candle = candle
if previous is None:
return
if self.Position != 0:
return
candle_time = candle.OpenTime.ToLocalTime()
if candle_time.DayOfWeek != DayOfWeek.Monday:
return
if candle_time.Hour != self.OpenHour:
return
if self._last_signal_time is not None and self._last_signal_time == candle.OpenTime:
return
typical_price = (float(previous.HighPrice) + float(previous.LowPrice) + float(previous.ClosePrice)) / 3.0
if float(candle.OpenPrice) <= typical_price:
return
volume = self._calculate_order_volume()
if volume <= 0:
return
self.BuyMarket(volume)
self._last_signal_time = candle.OpenTime
def _calculate_order_volume(self):
fixed_vol = float(self.FixedVolume)
if fixed_vol > 0:
return fixed_vol
min_volume = 0.01
if self.Security is not None and self.Security.MinVolume is not None and float(self.Security.MinVolume) > 0:
min_volume = float(self.Security.MinVolume)
equity = 0.0
if self.Portfolio is not None:
cv = self.Portfolio.CurrentValue
bv = self.Portfolio.BeginValue
if cv is not None and float(cv) > 0:
equity = float(cv)
elif bv is not None and float(bv) > 0:
equity = float(bv)
if equity <= 0:
return min_volume
initial_eq = float(self.InitialEquity)
if equity < initial_eq:
return min_volume
eq_step = float(self.EquityStep)
if eq_step <= 0:
return float(self.InitialStepVolume)
steps_decimal = (equity - initial_eq) / eq_step
if steps_decimal < 0:
steps_decimal = 0
steps = int(steps_decimal)
dynamic_volume = float(self.InitialStepVolume) + float(self.VolumeStep) * steps
if dynamic_volume < min_volume:
dynamic_volume = min_volume
return dynamic_volume
def OnReseted(self):
super(monday_typical_breakout_strategy, self).OnReseted()
self._previous_candle = None
self._last_signal_time = None
self._price_step = 0.0
def CreateClone(self):
return monday_typical_breakout_strategy()