Bull vs Medved 策略
Bull vs Medved 是从 MetaTrader 5 移植而来的限价突破策略。策略监控最近三根已完成的 K 线,只在一天内六个可配置的五分钟时间窗口内允许下单。当出现指定的多头或空头蜡烛组合时,策略会按照与当前买卖价差的固定偏移放置挂单,并使用对称的止损与止盈保护头寸。
交易逻辑
- 交易窗口:仅当当前时间处于六个时间窗口之一时才会评估信号(默认窗口为 00:05、04:05、08:05、12:05、16:05、20:05,每个窗口持续 5 分钟)。一旦离开窗口,单窗口只下一单的限制会被重置。
- 所需数据:策略至少等待三根完整的蜡烛。所有条件都基于最近三根已收盘蜡烛的开收价。
- 多头条件:
- 常规多头:倒数第三根蜡烛收于第二根的开盘价之上,第二根的实体至少为 1 个点,最近一根蜡烛的实体大于
CandleSizePips。 - 劣质多头过滤:若三根蜡烛的实体都很大且方向一致,则忽略信号,以避免追入过度拉升。
- “冷静多头”:若第二根蜡烛出现至少 2 个点的看跌回撤,则最新一根必须重新站上该回撤且实体不小于
CandleSizePips的 40%。常规多头(未被过滤)或“冷静多头”任意满足即可触发做多。 - 满足做多条件时,在最佳卖价下方
IndentUpPips的距离放置 buy limit 挂单(自动转换为价格单位)。
- 常规多头:倒数第三根蜡烛收于第二根的开盘价之上,第二根的实体至少为 1 个点,最近一根蜡烛的实体大于
- 空头条件:若最近一根蜡烛的看跌实体超过
CandleSizePips,则在最佳买价上方IndentDownPips位置放置 sell limit 挂单。 - 风控管理:订单成交后立即使用配置的点数距离附加绝对止损和止盈。
- 订单管理:每个时间窗口仅允许发送一张挂单,并且在同一标的仍有活动的限价单时不会再次下单。
参数
OrderVolume– 挂单的交易数量。CandleSizePips– 最近一根蜡烛所需的最小实体(点)。StopLossPips– 止损距离,按点数从入场价计算。TakeProfitPips– 止盈距离。IndentUpPips– buy limit 相对于最佳卖价的下移距离。IndentDownPips– sell limit 相对于最佳买价的上移距离。EntryWindowMinutes– 每个交易窗口的持续时间。CandleType– 用于判断形态的 K 线周期。StartTime0…StartTime5– 六个交易窗口的起始时间。
其他说明
- 策略订阅盘口数据以获得最新买价与卖价用于精确挂单;如果盘口暂不可用,则退化为使用最近一根蜡烛的收盘价。
- 所有距离会根据 3 位或 5 位报价自动转换为标准“点”单位。
- 止损/止盈通过
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bull vs Medved strategy converted from MetaTrader 5.
/// Enters market orders during predefined intraday windows when bullish or bearish candle sequences appear.
/// </summary>
public class BullVsMedvedStrategy : Strategy
{
private readonly StrategyParam<decimal> _candleSizePips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _entryWindowMinutes;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<TimeSpan> _startTime0;
private readonly StrategyParam<TimeSpan> _startTime1;
private readonly StrategyParam<TimeSpan> _startTime2;
private readonly StrategyParam<TimeSpan> _startTime3;
private readonly StrategyParam<TimeSpan> _startTime4;
private readonly StrategyParam<TimeSpan> _startTime5;
private decimal _pointValue;
private decimal _pipValue;
private decimal _bodyMinSize;
private decimal _pullbackSize;
private decimal _candleSizeThreshold;
private decimal _stopLossOffset;
private decimal _takeProfitOffset;
private bool _orderPlacedInWindow;
private ICandleMessage _previousCandle1;
private ICandleMessage _previousCandle2;
private TimeSpan[] _entryTimes = Array.Empty<TimeSpan>();
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public BullVsMedvedStrategy()
{
_candleSizePips = Param(nameof(CandleSizePips), 500m)
.SetDisplay("Candle Size (pips)", "Minimum body size for the latest candle", "Filters")
.SetGreaterThanZero()
.SetOptimize(25m, 150m, 25m);
_stopLossPips = Param(nameof(StopLossPips), 60m)
.SetDisplay("Stop Loss (pips)", "Distance from entry for protective stop", "Risk")
.SetGreaterThanZero()
.SetOptimize(20m, 120m, 20m);
_takeProfitPips = Param(nameof(TakeProfitPips), 60m)
.SetDisplay("Take Profit (pips)", "Distance from entry for profit target", "Risk")
.SetGreaterThanZero()
.SetOptimize(20m, 120m, 20m);
_entryWindowMinutes = Param(nameof(EntryWindowMinutes), 30)
.SetDisplay("Entry Window (min)", "Duration of each trading window", "Timing")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for pattern detection", "Data");
_startTime0 = Param(nameof(StartTime0), new TimeSpan(0, 0, 0))
.SetDisplay("Start Time #1", "First trading window start", "Timing");
_startTime1 = Param(nameof(StartTime1), new TimeSpan(4, 0, 0))
.SetDisplay("Start Time #2", "Second trading window start", "Timing");
_startTime2 = Param(nameof(StartTime2), new TimeSpan(8, 0, 0))
.SetDisplay("Start Time #3", "Third trading window start", "Timing");
_startTime3 = Param(nameof(StartTime3), new TimeSpan(12, 0, 0))
.SetDisplay("Start Time #4", "Fourth trading window start", "Timing");
_startTime4 = Param(nameof(StartTime4), new TimeSpan(16, 0, 0))
.SetDisplay("Start Time #5", "Fifth trading window start", "Timing");
_startTime5 = Param(nameof(StartTime5), new TimeSpan(20, 0, 0))
.SetDisplay("Start Time #6", "Sixth trading window start", "Timing");
}
/// <summary>
/// Minimum body size (in pips) required for the most recent candle.
/// </summary>
public decimal CandleSizePips
{
get => _candleSizePips.Value;
set => _candleSizePips.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Duration of each entry window in minutes.
/// </summary>
public int EntryWindowMinutes
{
get => _entryWindowMinutes.Value;
set => _entryWindowMinutes.Value = value;
}
/// <summary>
/// Candle type used to evaluate price patterns.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// First trading window start time.
/// </summary>
public TimeSpan StartTime0
{
get => _startTime0.Value;
set => _startTime0.Value = value;
}
/// <summary>
/// Second trading window start time.
/// </summary>
public TimeSpan StartTime1
{
get => _startTime1.Value;
set => _startTime1.Value = value;
}
/// <summary>
/// Third trading window start time.
/// </summary>
public TimeSpan StartTime2
{
get => _startTime2.Value;
set => _startTime2.Value = value;
}
/// <summary>
/// Fourth trading window start time.
/// </summary>
public TimeSpan StartTime3
{
get => _startTime3.Value;
set => _startTime3.Value = value;
}
/// <summary>
/// Fifth trading window start time.
/// </summary>
public TimeSpan StartTime4
{
get => _startTime4.Value;
set => _startTime4.Value = value;
}
/// <summary>
/// Sixth trading window start time.
/// </summary>
public TimeSpan StartTime5
{
get => _startTime5.Value;
set => _startTime5.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pointValue = 0m;
_pipValue = 0m;
_bodyMinSize = 0m;
_pullbackSize = 0m;
_candleSizeThreshold = 0m;
_stopLossOffset = 0m;
_takeProfitOffset = 0m;
_orderPlacedInWindow = false;
_previousCandle1 = null;
_previousCandle2 = null;
_entryTimes = Array.Empty<TimeSpan>();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var decimals = Security?.Decimals ?? 0;
var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
_pointValue = Security?.PriceStep ?? 1m;
_pipValue = _pointValue * adjust;
_bodyMinSize = 10m * _pointValue;
_pullbackSize = 20m * _pointValue;
_candleSizeThreshold = CandleSizePips * _pipValue;
_stopLossOffset = StopLossPips * _pipValue;
_takeProfitOffset = TakeProfitPips * _pipValue;
_entryTimes = new[]
{
StartTime0,
StartTime1,
StartTime2,
StartTime3,
StartTime4,
StartTime5
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(
stopLoss: _stopLossOffset > 0m ? new Unit(_stopLossOffset, UnitTypes.Absolute) : null,
takeProfit: _takeProfitOffset > 0m ? new Unit(_takeProfitOffset, UnitTypes.Absolute) : null,
useMarketOrders: true);
}
private void ProcessCandle(ICandleMessage candle)
{
// Ignore unfinished candles because their prices are not final.
if (candle.State != CandleStates.Finished)
return;
// Skip processing if trading environment is not ready.
if (!IsFormedAndOnlineAndAllowTrading())
return;
var inWindow = IsWithinEntryWindow(candle.CloseTime);
if (!inWindow)
{
_orderPlacedInWindow = false;
ShiftHistory(candle);
return;
}
if (_orderPlacedInWindow)
{
ShiftHistory(candle);
return;
}
if (_previousCandle1 is null || _previousCandle2 is null)
{
ShiftHistory(candle);
return;
}
var shift1 = candle;
var shift2 = _previousCandle1;
var shift3 = _previousCandle2;
var placedOrder = false;
var isBull = IsBull(shift3, shift2, shift1);
var isBadBull = IsBadBull(shift3, shift2, shift1);
var isCoolBull = IsCoolBull(shift2, shift1);
var isBear = IsBear(shift1);
if (isBull && !isBadBull)
placedOrder = TryBuy();
else if (isCoolBull)
placedOrder = TryBuy();
else if (isBear)
placedOrder = TrySell();
if (placedOrder)
_orderPlacedInWindow = true;
ShiftHistory(candle);
}
private bool IsWithinEntryWindow(DateTimeOffset time)
{
var window = TimeSpan.FromMinutes(EntryWindowMinutes);
var tod = time.TimeOfDay;
for (var i = 0; i < _entryTimes.Length; i++)
{
var start = _entryTimes[i];
var end = start + window;
if (tod >= start && tod <= end)
return true;
}
return false;
}
private bool TryBuy()
{
if (Position != 0)
return false;
BuyMarket();
return true;
}
private bool TrySell()
{
if (Position != 0)
return false;
SellMarket();
return true;
}
private bool IsBull(ICandleMessage shift3, ICandleMessage shift2, ICandleMessage shift1)
{
return shift3.ClosePrice > shift2.OpenPrice &&
(shift2.ClosePrice - shift2.OpenPrice) >= _bodyMinSize &&
(shift1.ClosePrice - shift1.OpenPrice) >= _candleSizeThreshold;
}
private bool IsBadBull(ICandleMessage shift3, ICandleMessage shift2, ICandleMessage shift1)
{
return (shift3.ClosePrice - shift3.OpenPrice) >= _bodyMinSize &&
(shift2.ClosePrice - shift2.OpenPrice) >= _bodyMinSize &&
(shift1.ClosePrice - shift1.OpenPrice) >= _candleSizeThreshold;
}
private bool IsCoolBull(ICandleMessage shift2, ICandleMessage shift1)
{
return (shift2.OpenPrice - shift2.ClosePrice) >= _pullbackSize &&
shift2.ClosePrice <= shift1.OpenPrice &&
shift1.ClosePrice > shift2.OpenPrice &&
(shift1.ClosePrice - shift1.OpenPrice) >= 0.4m * _candleSizeThreshold;
}
private bool IsBear(ICandleMessage shift1)
{
return (shift1.OpenPrice - shift1.ClosePrice) >= _candleSizeThreshold;
}
private void ShiftHistory(ICandleMessage candle)
{
_previousCandle2 = _previousCandle1;
_previousCandle1 = candle;
}
}
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 bull_vs_medved_strategy(Strategy):
"""Enters market orders during intraday windows when bullish or bearish candle sequences appear."""
def __init__(self):
super(bull_vs_medved_strategy, self).__init__()
self._candle_size_pips = self.Param("CandleSizePips", 500.0) \
.SetDisplay("Candle Size (pips)", "Minimum body size for the latest candle", "Filters") \
.SetGreaterThanZero()
self._stop_loss_pips = self.Param("StopLossPips", 60.0) \
.SetDisplay("Stop Loss (pips)", "Distance from entry for protective stop", "Risk") \
.SetGreaterThanZero()
self._take_profit_pips = self.Param("TakeProfitPips", 60.0) \
.SetDisplay("Take Profit (pips)", "Distance from entry for profit target", "Risk") \
.SetGreaterThanZero()
self._entry_window_minutes = self.Param("EntryWindowMinutes", 30) \
.SetDisplay("Entry Window (min)", "Duration of each trading window", "Timing") \
.SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Primary timeframe for pattern detection", "Data")
self._start_time0 = self.Param("StartTime0", TimeSpan(0, 0, 0)) \
.SetDisplay("Start Time #1", "First trading window start", "Timing")
self._start_time1 = self.Param("StartTime1", TimeSpan(4, 0, 0)) \
.SetDisplay("Start Time #2", "Second trading window start", "Timing")
self._start_time2 = self.Param("StartTime2", TimeSpan(8, 0, 0)) \
.SetDisplay("Start Time #3", "Third trading window start", "Timing")
self._start_time3 = self.Param("StartTime3", TimeSpan(12, 0, 0)) \
.SetDisplay("Start Time #4", "Fourth trading window start", "Timing")
self._start_time4 = self.Param("StartTime4", TimeSpan(16, 0, 0)) \
.SetDisplay("Start Time #5", "Fifth trading window start", "Timing")
self._start_time5 = self.Param("StartTime5", TimeSpan(20, 0, 0)) \
.SetDisplay("Start Time #6", "Sixth trading window start", "Timing")
self._point_value = 0.0
self._pip_value = 0.0
self._body_min_size = 0.0
self._pullback_size = 0.0
self._candle_size_threshold = 0.0
self._stop_loss_offset = 0.0
self._take_profit_offset = 0.0
self._order_placed_in_window = False
self._prev_candle1 = None
self._prev_candle2 = None
self._entry_times = []
@property
def CandleSizePips(self):
return self._candle_size_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def EntryWindowMinutes(self):
return self._entry_window_minutes.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(bull_vs_medved_strategy, self).OnStarted2(time)
sec = self.Security
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 0
adjust = 10.0 if decimals == 3 or decimals == 5 else 1.0
self._point_value = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
self._pip_value = self._point_value * adjust
self._body_min_size = 10.0 * self._point_value
self._pullback_size = 20.0 * self._point_value
self._candle_size_threshold = float(self.CandleSizePips) * self._pip_value
self._stop_loss_offset = float(self.StopLossPips) * self._pip_value
self._take_profit_offset = float(self.TakeProfitPips) * self._pip_value
self._entry_times = [
self._start_time0.Value,
self._start_time1.Value,
self._start_time2.Value,
self._start_time3.Value,
self._start_time4.Value,
self._start_time5.Value,
]
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self.process_candle) \
.Start()
sl_unit = Unit(self._stop_loss_offset, UnitTypes.Absolute) if self._stop_loss_offset > 0 else None
tp_unit = Unit(self._take_profit_offset, UnitTypes.Absolute) if self._take_profit_offset > 0 else None
self.StartProtection(sl_unit, tp_unit, True)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._shift(candle)
return
in_window = self._is_within_window(candle.CloseTime)
if not in_window:
self._order_placed_in_window = False
self._shift(candle)
return
if self._order_placed_in_window:
self._shift(candle)
return
if self._prev_candle1 is None or self._prev_candle2 is None:
self._shift(candle)
return
s1 = candle
s2 = self._prev_candle1
s3 = self._prev_candle2
placed = False
is_bull = self._is_bull(s3, s2, s1)
is_bad_bull = self._is_bad_bull(s3, s2, s1)
is_cool_bull = self._is_cool_bull(s2, s1)
is_bear = self._is_bear(s1)
if is_bull and not is_bad_bull:
placed = self._try_buy()
elif is_cool_bull:
placed = self._try_buy()
elif is_bear:
placed = self._try_sell()
if placed:
self._order_placed_in_window = True
self._shift(candle)
def _is_within_window(self, time):
window = TimeSpan.FromMinutes(self.EntryWindowMinutes)
tod = time.TimeOfDay
for et in self._entry_times:
end = et + window
if tod >= et and tod <= end:
return True
return False
def _try_buy(self):
if self.Position != 0:
return False
self.BuyMarket()
return True
def _try_sell(self):
if self.Position != 0:
return False
self.SellMarket()
return True
def _is_bull(self, s3, s2, s1):
return (float(s3.ClosePrice) > float(s2.OpenPrice) and
float(s2.ClosePrice) - float(s2.OpenPrice) >= self._body_min_size and
float(s1.ClosePrice) - float(s1.OpenPrice) >= self._candle_size_threshold)
def _is_bad_bull(self, s3, s2, s1):
return (float(s3.ClosePrice) - float(s3.OpenPrice) >= self._body_min_size and
float(s2.ClosePrice) - float(s2.OpenPrice) >= self._body_min_size and
float(s1.ClosePrice) - float(s1.OpenPrice) >= self._candle_size_threshold)
def _is_cool_bull(self, s2, s1):
return (float(s2.OpenPrice) - float(s2.ClosePrice) >= self._pullback_size and
float(s2.ClosePrice) <= float(s1.OpenPrice) and
float(s1.ClosePrice) > float(s2.OpenPrice) and
float(s1.ClosePrice) - float(s1.OpenPrice) >= 0.4 * self._candle_size_threshold)
def _is_bear(self, s1):
return float(s1.OpenPrice) - float(s1.ClosePrice) >= self._candle_size_threshold
def _shift(self, candle):
self._prev_candle2 = self._prev_candle1
self._prev_candle1 = candle
def OnReseted(self):
super(bull_vs_medved_strategy, self).OnReseted()
self._point_value = 0.0
self._pip_value = 0.0
self._body_min_size = 0.0
self._pullback_size = 0.0
self._candle_size_threshold = 0.0
self._stop_loss_offset = 0.0
self._take_profit_offset = 0.0
self._order_placed_in_window = False
self._prev_candle1 = None
self._prev_candle2 = None
self._entry_times = []
def CreateClone(self):
return bull_vs_medved_strategy()