Fractals at Close Prices 策略
概述
该策略是 MetaTrader 5 专家顾问 “Fractals at Close prices”(作者 Vladimir Karputov)的 StockSharp 版本。策略跟踪连续五根K线的收盘价,并仅使用收盘价来构建 Bill Williams 风格的分形。当最新的多头分形位于上一个多头分形之上时,视为趋势向上;当最新的空头分形位于上一个空头分形之下时,视为趋势向下。在执行新的方向信号前,会先平掉相反方向的仓位,因此策略同一时间只持有多头或空头。
交易只允许发生在可配置的开始小时与结束小时之间。如果当前小时不在时间窗内,策略会立即平掉所有仓位,这与原始EA的行为一致。时间过滤器支持日内区间(start < end)、跨越午夜的区间(start > end)以及全天交易(start == end)。
分形判定逻辑
- 每根收盘完成的K线都会被加入一个包含五个元素的滚动队列。
- 当窗口被填满时,会评估中间的收盘价(即两根K线之前的收盘):
- 若中间值严格大于两根更早的收盘价,并且大于或等于两根更新的收盘价,则记录一个多头分形。
- 若中间值严格小于两根更早的收盘价,并且小于或等于两根更新的收盘价,则记录一个空头分形。
- 保存最近与上一个多头分形,以及最近与上一个空头分形,供后续比较。
- 若最新的多头分形高于上一个多头分形,则视为多头趋势;若最新的空头分形低于上一个空头分形,则视为空头趋势。
交易规则
- 开多
- 先以市价平掉所有空头仓位。
- 如果当前没有多头仓位,则在确认多头分形序列的收盘价处买入
OrderVolume。
- 开空
- 先以市价平掉所有多头仓位。
- 如果当前没有空头仓位,则在确认空头分形序列时卖出
OrderVolume。
- 交易时段控制
- 在处理信号之前,策略会检查
candle.OpenTime.Hour是否位于时间窗口内。如果不满足条件,则调用CloseAllPositions并忽略该根K线。
- 在处理信号之前,策略会检查
风险控制
- 止损和止盈距离以“点”(pip)表示,实现方式与 MT5 相同:当品种具有 3 或 5 位小数时,会将价格最小变动乘以10,再与设置的点数相乘得到真实价格距离。
- 开仓时会把初始止损和止盈价保存在内部变量中。由于 StockSharp 不会像 MT5 那样自动管理保护性订单,策略会在每根K线收盘后检查是否触及这些价格,一旦触及便以市价平仓。
- 移动止损沿用原 EA 的逻辑:当浮动利润超过
TrailingStop + TrailingStep时,将新的止损设为close ± TrailingStop,且仅当新止损与旧止损的距离至少为TrailingStep时才会更新。 - 当当前时间超出交易窗口时,不论移动止损状态如何,都会立即平掉全部仓位。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
OrderVolume |
每次市价单的交易量。 | 0.1 |
StartHour |
允许开仓的起始小时(0-23)。若与 EndHour 相等则表示全天交易。 |
10 |
EndHour |
停止开仓的小时(0-23)。 | 22 |
StopLossPips |
止损距离(点)。为 0 时关闭止损。 |
30 |
TakeProfitPips |
止盈距离(点)。为 0 时关闭止盈。 |
50 |
TrailingStopPips |
移动止损的基础距离(点)。为 0 时关闭移动止损。 |
15 |
TrailingStepPips |
移动止损每次调整所需的额外利润(点)。 | 5 |
CandleType |
策略订阅的K线类型,默认使用1小时周期。 | 1 hour TimeFrame |
实现说明
- 策略使用高阶 API
SubscribeCandles,不直接向Indicators集合注册指标,符合项目约定。 - 止损、止盈和移动止损通过在K线收盘后发送市价单来执行,以模拟 MT5 中的保护性订单行为。
- 时间过滤、分形判定与移动止损逻辑均遵循原始 EA 的结构,包括在时间窗口外强制平仓。
- 点值转换完全复制 MT5 方案:当小数位为 3 或 5 时,将价格步长乘以10,以获得等价的价格距离。
使用建议
- 将策略绑定到目标品种,并设置合适的
OrderVolume。 - 选择与 MT5 中相同的时间周期,以便获得可比的信号。
- 根据经纪商交易时段或个人需求调整时间窗口。
- 根据品种波动性调整各项点值参数。较大的
TrailingStepPips会降低移动止损调整频率,较小的数值则会更紧密地跟随价格。 - 关注日志与可选的图表绘制,以便及时验证策略行为。
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>
/// Strategy converted from the MT5 "Fractals at Close prices" expert advisor.
/// Detects bullish and bearish fractal sequences built on close prices and trades trend reversals.
/// Includes configurable trading hours and manual risk management with trailing stops.
/// </summary>
public class FractalsAtClosePricesStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeWindow = new(6);
private decimal? _lastUpperFractal;
private decimal? _previousUpperFractal;
private decimal? _lastLowerFractal;
private decimal? _previousLowerFractal;
private decimal _pipValue;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _trailingStopDistance;
private decimal _trailingStepDistance;
private decimal? _entryPrice;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortStop;
private decimal? _shortTake;
/// <summary>
/// Trading volume used for every market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Hour when the strategy can start opening positions.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour when the strategy stops opening positions.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Stop-loss size expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit size expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum price improvement required before moving the trailing stop.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="FractalsAtClosePricesStrategy"/> parameters.
/// </summary>
public FractalsAtClosePricesStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume used for entries", "General")
;
_startHour = Param(nameof(StartHour), 0)
.SetRange(0, 23)
.SetDisplay("Start Hour", "Hour when trading can start (0-23)", "Trading Hours");
_endHour = Param(nameof(EndHour), 0)
.SetRange(0, 23)
.SetDisplay("End Hour", "Hour when trading stops (0-23)", "Trading Hours");
_stopLossPips = Param(nameof(StopLossPips), 200)
.SetRange(0, 1000)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 400)
.SetRange(0, 1000)
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 15)
.SetRange(0, 1000)
.SetDisplay("Trailing Stop (pips)", "Base distance for the trailing stop", "Risk Management")
;
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetRange(0, 1000)
.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk Management")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles processed by the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closeWindow.Clear();
_lastUpperFractal = null;
_previousUpperFractal = null;
_lastLowerFractal = null;
_previousLowerFractal = null;
_pipValue = 0m;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_trailingStopDistance = 0m;
_trailingStepDistance = 0m;
_entryPrice = null;
ResetRiskLevels();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var priceStep = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals ?? 0;
_pipValue = priceStep;
if (decimals == 3 || decimals == 5)
{
// MT5 version multiplies point value by 10 when the symbol uses 3 or 5 decimals.
_pipValue *= 10m;
}
_stopLossDistance = StopLossPips == 0 ? 0m : StopLossPips * _pipValue;
_takeProfitDistance = TakeProfitPips == 0 ? 0m : TakeProfitPips * _pipValue;
_trailingStopDistance = TrailingStopPips == 0 ? 0m : TrailingStopPips * _pipValue;
_trailingStepDistance = TrailingStepPips == 0 ? 0m : TrailingStepPips * _pipValue;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
{
return;
}
UpdateFractals(candle);
if (!IsWithinTradingHours(candle.OpenTime))
{
CloseAllPositions();
return;
}
ApplyRiskManagement(candle);
// no bound indicators, skip IsFormedAndOnlineAndAllowTrading()
ExecuteEntries(candle);
}
private void UpdateFractals(ICandleMessage candle)
{
// Maintain a rolling window of the five most recent closes.
_closeWindow.Add(candle.ClosePrice);
while (_closeWindow.Count > 5)
_closeWindow.RemoveAt(0);
if (_closeWindow.Count < 5)
{
return;
}
var window = _closeWindow;
var center = window[2];
var isUpper = center > window[0]
&& center > window[1]
&& center >= window[3]
&& center >= window[4];
if (isUpper)
{
_previousUpperFractal = _lastUpperFractal;
_lastUpperFractal = center;
}
var isLower = center < window[0]
&& center < window[1]
&& center <= window[3]
&& center <= window[4];
if (isLower)
{
_previousLowerFractal = _lastLowerFractal;
_lastLowerFractal = center;
}
}
private bool IsWithinTradingHours(DateTimeOffset time)
{
var hour = time.Hour;
if (StartHour == EndHour)
{
// Trade the entire day when start and end hours are equal.
return true;
}
if (StartHour < EndHour)
{
return hour >= StartHour && hour < EndHour;
}
return hour >= StartHour || hour < EndHour;
}
private void ApplyRiskManagement(ICandleMessage candle)
{
if (Position > 0)
{
if (_longStop is decimal stop && candle.LowPrice <= stop)
{
// Close the long position if the stop-loss level is breached.
SellMarket(Position);
ResetRiskLevels();
return;
}
if (_longTake is decimal take && candle.HighPrice >= take)
{
// Close the long position when the take-profit level is hit.
SellMarket(Position);
ResetRiskLevels();
return;
}
UpdateLongTrailingStop(candle);
}
else if (Position < 0)
{
if (_shortStop is decimal stop && candle.HighPrice >= stop)
{
// Cover the short position if the stop-loss level is breached.
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
return;
}
if (_shortTake is decimal take && candle.LowPrice <= take)
{
// Cover the short position when the take-profit level is hit.
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
return;
}
UpdateShortTrailingStop(candle);
}
}
private void UpdateLongTrailingStop(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
{
return;
}
var profitDistance = candle.ClosePrice - entry;
if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
{
return;
}
var targetStop = candle.ClosePrice - _trailingStopDistance;
if (_longStop is decimal currentStop && currentStop >= candle.ClosePrice - (_trailingStopDistance + _trailingStepDistance))
{
// Skip updates until price improved by the trailing step.
return;
}
_longStop = targetStop;
}
private void UpdateShortTrailingStop(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
{
return;
}
var profitDistance = entry - candle.ClosePrice;
if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
{
return;
}
var targetStop = candle.ClosePrice + _trailingStopDistance;
if (_shortStop is decimal currentStop && currentStop <= candle.ClosePrice + (_trailingStopDistance + _trailingStepDistance))
{
// Skip updates until price improved by the trailing step.
return;
}
_shortStop = targetStop;
}
private void ExecuteEntries(ICandleMessage candle)
{
// Only trade when flat to avoid too frequent reversals.
if (Position != 0)
return;
var bullishTrend = _lastLowerFractal is decimal lastLow
&& _previousLowerFractal is decimal prevLow
&& prevLow < lastLow;
if (bullishTrend && OrderVolume > 0m)
{
BuyMarket(OrderVolume);
_entryPrice = candle.ClosePrice;
_longStop = _stopLossDistance > 0m ? candle.ClosePrice - _stopLossDistance : null;
_longTake = _takeProfitDistance > 0m ? candle.ClosePrice + _takeProfitDistance : null;
_shortStop = null;
_shortTake = null;
return;
}
var bearishTrend = _lastUpperFractal is decimal lastUp
&& _previousUpperFractal is decimal prevUp
&& prevUp > lastUp;
if (bearishTrend && OrderVolume > 0m)
{
SellMarket(OrderVolume);
_entryPrice = candle.ClosePrice;
_shortStop = _stopLossDistance > 0m ? candle.ClosePrice + _stopLossDistance : null;
_shortTake = _takeProfitDistance > 0m ? candle.ClosePrice - _takeProfitDistance : null;
_longStop = null;
_longTake = null;
}
}
private void CloseAllPositions()
{
if (Position > 0)
{
SellMarket(Position);
}
else if (Position < 0)
{
BuyMarket(Math.Abs(Position));
}
ResetRiskLevels();
}
private void CloseLongPosition()
{
if (Position > 0)
{
SellMarket(Position);
ResetRiskLevels();
}
}
private void CloseShortPosition()
{
if (Position < 0)
{
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
}
}
private void ResetRiskLevels()
{
_longStop = null;
_longTake = null;
_shortStop = null;
_shortTake = null;
_entryPrice = 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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class fractals_at_close_prices_strategy(Strategy):
def __init__(self):
super(fractals_at_close_prices_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 0)
self._end_hour = self.Param("EndHour", 0)
self._stop_loss_pips = self.Param("StopLossPips", 200)
self._take_profit_pips = self.Param("TakeProfitPips", 400)
self._trailing_stop_pips = self.Param("TrailingStopPips", 15)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._pip_value = 0.0
self._sl_dist = 0.0
self._tp_dist = 0.0
self._trail_dist = 0.0
self._trail_step = 0.0
self._entry_price = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.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
def OnStarted2(self, time):
super(fractals_at_close_prices_strategy, self).OnStarted2(time)
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 0
self._pip_value = price_step
if decimals == 3 or decimals == 5:
self._pip_value *= 10.0
self._sl_dist = self.StopLossPips * self._pip_value if self.StopLossPips != 0 else 0.0
self._tp_dist = self.TakeProfitPips * self._pip_value if self.TakeProfitPips != 0 else 0.0
self._trail_dist = self.TrailingStopPips * self._pip_value if self.TrailingStopPips != 0 else 0.0
self._trail_step = self.TrailingStepPips * self._pip_value if self.TrailingStepPips != 0 else 0.0
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._entry_price = None
self._reset_risk_levels()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_fractals(candle)
if not self._is_within_trading_hours(candle.OpenTime):
self._close_all()
return
self._apply_risk_management(candle)
self._execute_entries(candle)
def _update_fractals(self, candle):
self._close_window.append(float(candle.ClosePrice))
while len(self._close_window) > 5:
self._close_window.pop(0)
if len(self._close_window) < 5:
return
w = self._close_window
center = w[2]
is_upper = (center > w[0] and center > w[1] and
center >= w[3] and center >= w[4])
if is_upper:
self._prev_upper_fractal = self._last_upper_fractal
self._last_upper_fractal = center
is_lower = (center < w[0] and center < w[1] and
center <= w[3] and center <= w[4])
if is_lower:
self._prev_lower_fractal = self._last_lower_fractal
self._last_lower_fractal = center
def _is_within_trading_hours(self, time):
hour = time.Hour
if self.StartHour == self.EndHour:
return True
if self.StartHour < self.EndHour:
return hour >= self.StartHour and hour < self.EndHour
return hour >= self.StartHour or hour < self.EndHour
def _apply_risk_management(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position > 0:
if self._long_stop is not None and low <= self._long_stop:
self.SellMarket()
self._reset_risk_levels()
return
if self._long_take is not None and high >= self._long_take:
self.SellMarket()
self._reset_risk_levels()
return
self._update_long_trailing(candle)
elif self.Position < 0:
if self._short_stop is not None and high >= self._short_stop:
self.BuyMarket()
self._reset_risk_levels()
return
if self._short_take is not None and low <= self._short_take:
self.BuyMarket()
self._reset_risk_levels()
return
self._update_short_trailing(candle)
def _update_long_trailing(self, candle):
if self._trail_dist <= 0 or self._entry_price is None:
return
close = float(candle.ClosePrice)
profit_dist = close - self._entry_price
if profit_dist <= self._trail_dist + self._trail_step:
return
target_stop = close - self._trail_dist
if self._long_stop is not None and self._long_stop >= close - (self._trail_dist + self._trail_step):
return
self._long_stop = target_stop
def _update_short_trailing(self, candle):
if self._trail_dist <= 0 or self._entry_price is None:
return
close = float(candle.ClosePrice)
profit_dist = self._entry_price - close
if profit_dist <= self._trail_dist + self._trail_step:
return
target_stop = close + self._trail_dist
if self._short_stop is not None and self._short_stop <= close + (self._trail_dist + self._trail_step):
return
self._short_stop = target_stop
def _execute_entries(self, candle):
if self.Position != 0:
return
close = float(candle.ClosePrice)
bullish_trend = (self._last_lower_fractal is not None and
self._prev_lower_fractal is not None and
self._prev_lower_fractal < self._last_lower_fractal)
if bullish_trend:
self.BuyMarket()
self._entry_price = close
self._long_stop = close - self._sl_dist if self._sl_dist > 0 else None
self._long_take = close + self._tp_dist if self._tp_dist > 0 else None
self._short_stop = None
self._short_take = None
return
bearish_trend = (self._last_upper_fractal is not None and
self._prev_upper_fractal is not None and
self._prev_upper_fractal > self._last_upper_fractal)
if bearish_trend:
self.SellMarket()
self._entry_price = close
self._short_stop = close + self._sl_dist if self._sl_dist > 0 else None
self._short_take = close - self._tp_dist if self._tp_dist > 0 else None
self._long_stop = None
self._long_take = None
def _close_all(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_risk_levels()
def _reset_risk_levels(self):
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._entry_price = None
def OnReseted(self):
super(fractals_at_close_prices_strategy, self).OnReseted()
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._pip_value = 0.0
self._sl_dist = 0.0
self._tp_dist = 0.0
self._trail_dist = 0.0
self._trail_step = 0.0
self._entry_price = None
self._reset_risk_levels()
def CreateClone(self):
return fractals_at_close_prices_strategy()