分形最小距离策略
概述
分形最小距离策略在 StockSharp 高层 API 中重现 MetaTrader 专家顾问“Fractals minimum distance”的逻辑。策略订阅所选的 K 线序列,在每根新 K 线结束后检查信号栏位(SignalBar)处是否生成了新的比尔·威廉姆斯五根 K 线分形。只有当最新的上分形和下分形之间的间距(以点数 DistancePips 表示)达到要求时才允许开仓。
该移植版本保留了原程序在入场前先平掉反向仓位的处理方式,并同样不设置止损或止盈。与 MQL 版本不同的是,仓位大小不再根据账户风险计算,而是直接使用策略的 Volume 属性。
信号逻辑
- 根据
CandleType订阅蜡烛数据,并维护一个滑动窗口,窗口中总能包含信号栏位以及其左右各两个邻近的高低点。 - 当窗口中心的最高价大于前两根和后两根 K 线的最高价时确认上分形;当中心的最低价小于左右两侧各两根 K 线的最低价时确认下分形。
- 将
DistancePips乘以交易品种的PriceStep转换为实际价格距离。对于三位或五位小数报价,会自动将 0.001/0.00001 视为一个点。 - 确认上分形时:
- 记录新的上分形价位,并平掉所有多头仓位。
- 若最近的上下分形之间的距离不小于阈值,并且策略允许交易,则按
Volume下市价卖单。
- 确认下分形时:
- 记录新的下分形价位,并平掉所有空头仓位。
- 在满足距离条件时按
Volume下市价买单。
所有判断都在 K 线收盘后执行,因此尚未完成的 K 线不会触发信号。策略依赖 IsFormedAndOnlineAndAllowTrading() 来确认终端已准备就绪。
参数说明
| 参数 | 含义 | 备注 |
|---|---|---|
DistancePips |
最近上分形与下分形之间的最小间距,单位为点(pip)。 | 会根据 PriceStep 自动转换为价格单位。 |
SignalBar |
信号分形所在的历史 K 线距离当前的根数。 | 实际有效的最小值为 2,符合分形左右各两根 K 线的定义。 |
CandleType |
用于计算的蜡烛数据类型。 | 默认是一分钟 K 线,可根据需要改为其他周期。 |
Volume |
交易量,来自 Strategy 基类的属性。 | 取代原专家顾问的风险比例开仓方式。 |
与 MQL 版本的差异
- 继续在反向开仓前平掉已有仓位,与源代码中的
ClosePositions函数一致。 - 原程序中的
RefreshRates、滑点设置和下单验证交由 StockSharp 的撮合层处理。 - 没有添加止损/止盈逻辑,以保持与原版一致。
DistancePips和SignalBar分别对应 MQL 的ushort与uchar输入,保留整型语义。- StockSharp 采用净头寸模式,因此反向下单会直接翻转仓位,行为与 MetaTrader 净额账户相符。
使用建议
- 起步时可使用默认的
SignalBar = 3,再根据交易品种波动性调整DistancePips。 - 若市场波动较大,可增加
SignalBar以等待更多确认蜡烛,从而减少噪音信号。 - 如需风控保护,可结合
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Fractals minimum distance breakout strategy converted from MetaTrader.
/// </summary>
public class FractalsMinimumDistanceStrategy : Strategy
{
private readonly StrategyParam<int> _distancePips;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevUpperFractal;
private decimal? _prevLowerFractal;
private decimal[] _highs = Array.Empty<decimal>();
private decimal[] _lows = Array.Empty<decimal>();
private int _bufferCount;
private int _windowSize;
private int _signalOffset;
private decimal _pipSize;
public int DistancePips
{
get => _distancePips.Value;
set => _distancePips.Value = value;
}
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public FractalsMinimumDistanceStrategy()
{
_distancePips = Param(nameof(DistancePips), 15)
.SetDisplay("Distance (pips)", "Minimum allowed gap between the last two fractals", "Risk")
;
_signalBar = Param(nameof(SignalBar), 3)
.SetDisplay("Signal bar offset", "How many closed bars ago the fractal must appear", "General")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle type", "Primary candle series used for signals", "Data")
;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevUpperFractal = null;
_prevLowerFractal = null;
_highs = Array.Empty<decimal>();
_lows = Array.Empty<decimal>();
_bufferCount = 0;
_windowSize = 0;
_signalOffset = 0;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
InitializeBuffers();
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void InitializeBuffers()
{
_signalOffset = Math.Max(2, SignalBar);
_windowSize = Math.Max(_signalOffset + 3, 5);
_highs = new decimal[_windowSize];
_lows = new decimal[_windowSize];
_bufferCount = 0;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _windowSize == 0)
return;
// Shift the rolling buffers to keep the configured number of historical bars.
for (var i = 0; i < _windowSize - 1; i++)
{
_highs[i] = _highs[i + 1];
_lows[i] = _lows[i + 1];
}
// Append the latest candle extremes.
_highs[_windowSize - 1] = candle.HighPrice;
_lows[_windowSize - 1] = candle.LowPrice;
if (_bufferCount < _windowSize)
_bufferCount++;
if (_bufferCount < _windowSize)
return;
var centerIndex = _windowSize - 1 - _signalOffset;
if (centerIndex < 2 || centerIndex > _windowSize - 3)
return;
var high = _highs[centerIndex];
var low = _lows[centerIndex];
var isUpperFractal =
high > _highs[centerIndex - 1] &&
high > _highs[centerIndex - 2] &&
high > _highs[centerIndex + 1] &&
high > _highs[centerIndex + 2];
var isLowerFractal =
low < _lows[centerIndex - 1] &&
low < _lows[centerIndex - 2] &&
low < _lows[centerIndex + 1] &&
low < _lows[centerIndex + 2];
var distanceThreshold = DistancePips * _pipSize;
if (isUpperFractal)
{
_prevUpperFractal = high;
// Close existing long exposure before reversing.
if (Position > 0)
SellMarket();
// Enter a short position if the fractals are far enough apart.
if (ShouldOpenTrade(distanceThreshold))
SellMarket();
}
if (isLowerFractal)
{
_prevLowerFractal = low;
// Close existing short exposure before reversing.
if (Position < 0)
BuyMarket();
// Enter a long position if the fractals are far enough apart.
if (ShouldOpenTrade(distanceThreshold))
BuyMarket();
}
}
private bool ShouldOpenTrade(decimal distanceThreshold)
{
if (Volume <= 0)
return false;
if (_prevUpperFractal is not decimal upper || _prevLowerFractal is not decimal lower)
return false;
var threshold = Math.Abs(distanceThreshold);
return Math.Abs(upper - lower) >= threshold;
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep;
if (priceStep is not decimal step || step <= 0m)
return 1m;
var decimals = GetDecimalPlaces(step);
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private static int GetDecimalPlaces(decimal value)
{
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
}
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_minimum_distance_strategy(Strategy):
"""Fractals minimum distance breakout strategy."""
def __init__(self):
super(fractals_minimum_distance_strategy, self).__init__()
self._distance_pips = self.Param("DistancePips", 15) \
.SetDisplay("Distance (pips)", "Minimum allowed gap between fractals", "Risk")
self._signal_bar = self.Param("SignalBar", 3) \
.SetDisplay("Signal bar offset", "How many closed bars ago the fractal must appear", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle type", "Primary candle series used for signals", "Data")
self._prev_upper = None
self._prev_lower = None
self._highs = []
self._lows = []
self._buffer_count = 0
self._window_size = 0
self._signal_offset = 0
self._pip_size = 0.0
@property
def DistancePips(self):
return self._distance_pips.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 1.0
step = float(sec.PriceStep)
# count decimals
digits = 0
temp = step
while temp > 0 and temp < 1 and digits < 10:
temp *= 10
digits += 1
if digits == 3 or digits == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(fractals_minimum_distance_strategy, self).OnStarted2(time)
self._signal_offset = max(2, self.SignalBar)
self._window_size = max(self._signal_offset + 3, 5)
self._highs = [0.0] * self._window_size
self._lows = [0.0] * self._window_size
self._buffer_count = 0
self._pip_size = self._calc_pip_size()
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 or self._window_size == 0:
return
ws = self._window_size
for i in range(ws - 1):
self._highs[i] = self._highs[i + 1]
self._lows[i] = self._lows[i + 1]
self._highs[ws - 1] = float(candle.HighPrice)
self._lows[ws - 1] = float(candle.LowPrice)
if self._buffer_count < ws:
self._buffer_count += 1
if self._buffer_count < ws:
return
ci = ws - 1 - self._signal_offset
if ci < 2 or ci > ws - 3:
return
high = self._highs[ci]
low = self._lows[ci]
is_upper = (high > self._highs[ci - 1] and high > self._highs[ci - 2] and
high > self._highs[ci + 1] and high > self._highs[ci + 2])
is_lower = (low < self._lows[ci - 1] and low < self._lows[ci - 2] and
low < self._lows[ci + 1] and low < self._lows[ci + 2])
dist_threshold = self.DistancePips * self._pip_size
if is_upper:
self._prev_upper = high
if self.Position > 0:
self.SellMarket()
if self._should_open(dist_threshold):
self.SellMarket()
if is_lower:
self._prev_lower = low
if self.Position < 0:
self.BuyMarket()
if self._should_open(dist_threshold):
self.BuyMarket()
def _should_open(self, threshold):
if self.Volume <= 0:
return False
if self._prev_upper is None or self._prev_lower is None:
return False
return abs(self._prev_upper - self._prev_lower) >= abs(threshold)
def OnReseted(self):
super(fractals_minimum_distance_strategy, self).OnReseted()
self._prev_upper = None
self._prev_lower = None
self._highs = []
self._lows = []
self._buffer_count = 0
self._window_size = 0
self._signal_offset = 0
self._pip_size = 0.0
def CreateClone(self):
return fractals_minimum_distance_strategy()