SendClose 策略
概述
SendClose 是一套基于分形支撑/阻力的突破系统,来源于 MT5 平台的同名专家顾问。策略通过连接交替出现的上/下分形来构建动态趋势线,一旦价格重新触及这些投射出来的水平,就会执行开仓或平仓操作。此移植版完全基于 StockSharp 的高级 API,实现了与原版相同的交易规则:
- 使用最近的分形节点绘制卖出线与买入线;
- 价格触线后采用市价单入场;
- 通过在趋势线上下方平移固定点数生成 Close1/Close2 退出线,用于强制平仓。
分形识别流程
- 五根K线窗口:策略维护最近五根已完成的K线缓冲区,当数据充足时,总是检查位于中间的那根K线。
- 上分形条件:若中间K线的最高价高于其两根更早K线,并且不低于两根更晚K线的最高价,则确认一个上分形,与 MT5 的
iFractals指标保持一致。 - 下分形条件:若中间K线的最低价低于其两根更早K线,并且不高于两根更晚K线的最低价,则确认一个下分形。
- 分形队列:每次确认新的分形后,都会压入一个最多六个元素的队列中,按时间从新到旧排列,供趋势线构建逻辑检索。
趋势线构建
- 卖出线:寻找最近一次“上分形 → 下分形 → 上分形”的组合,并将两端的上分形连接成直线,对应阻力线。
- 买入线:寻找最近一次“下分形 → 上分形 → 下分形”的组合,并连接两端的下分形,形成支撑线。
- 价格投射:保存分形点的时间与价格后,可在任何未来时刻对直线进行插值或外推,得到当前蜡烛收盘时间的理论价格。
- 退出线:在卖出线上方、买入线下方分别平移
LineOffsetSteps × PriceStep,生成 Close1 与 Close2,用于复制原策略的强制平仓机制。
交易逻辑
入场
- 卖出:当价格触及卖出线,且当前不存在多头头寸时,发送卖出市价单。若已经持有空头仓位,可在不超过
MaxPositions限额的前提下加仓。 - 买入:当价格触及买入线,且当前不存在空头头寸时,发送买入市价单。同样允许在限额内逐步加仓。
离场
- Close1/Close2:价格触碰任一退出线时,立即平掉全部持仓,与 MT5 版本保持一致。
- 净额处理:由于 StockSharp 采用净头寸模型,策略会在信号触发时先尝试平掉反向仓位,再决定是否开立新方向的头寸。
触线判定
原版在报价触及直线时即时反应,此实现使用蜡烛的高低价区间来近似。如果需要更高精度,可改用逐笔数据订阅。
参数说明
| 参数 | 说明 |
|---|---|
EnableSellLine |
是否根据上方分形线执行卖出入场。 |
EnableBuyLine |
是否根据下方分形线执行买入入场。 |
EnableCloseSellLine |
是否启用 Close1(卖出线向上平移后的平仓线)。 |
EnableCloseBuyLine |
是否启用 Close2(买入线向下平移后的平仓线)。 |
MaxPositions |
允许同时持有的最大净头寸(以下单手数为单位)。 |
OrderVolume |
每次市价单的下单数量。 |
LineOffsetSteps |
Close1/Close2 与基础趋势线之间的价格步长偏移,默认 15,对应 MT5 中的 15*Point()。 |
CandleType |
用于计算的K线类型(时间框架)。 |
实现细节
- 仅处理已完成的K线,避免在未确认的蜡烛上产生虚假分形。
- 退出逻辑优先于入场逻辑,保证触发平仓时不会同时再开仓。
MaxPositions基于净仓计算,因此在净额账户中可直接限制加仓数量;若需要完全复制 MT5 的对冲模式,可将该值设置为更大并在交易所层面允许对冲。- 偏移量依赖
Security.PriceStep。若交易品种未提供价格步长,请手动设置或在外部补充。
使用建议
- 在 StockSharp 终端中选定交易品种,确认其最小价格变动(PriceStep)与合约规模信息完整。
- 将
CandleType设置为与图表一致的时间框架,如 M15 或 H1,以获得与原策略相近的信号节奏。 - 根据资金管理需求调整
OrderVolume与MaxPositions,控制最大敞口。 - 如果市场波动较大,可适当提高
LineOffsetSteps,以减少噪音触发的平仓。 - 建议结合账户级风控(如日内止损、交易时段过滤)一起使用,防止无保护的市价单造成过度亏损。
与 MT5 版本的差异
- 新版本不自动绘制图形对象,如需可视化,可在图表模块中自行绘制趋势线。
- 使用蜡烛高低价替代买卖价判断触线,信号可能比 MT5 的逐笔触发略有延迟。
- 多空互转时会优先平仓再入场,避免在净额账户里出现双向持仓。
风险提示
该策略未内置止损或资金管理规则,仅依赖分形线触发。实盘前务必在目标品种上进行充分回测与前向验证,并辅以账户级风险控制措施。
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>
/// SendClose strategy replicates fractal breakout lines with close-based exits.
/// This class recreates the MT5 SendClose expert using StockSharp high level API.
/// </summary>
public class SendCloseStrategy : Strategy
{
private enum FractalTypes
{
Up,
Down
}
private readonly struct FractalPoint
{
public FractalPoint(FractalTypes type, DateTimeOffset time, decimal price)
{
Type = type;
Time = time;
Price = price;
}
public FractalTypes Type { get; }
public DateTimeOffset Time { get; }
public decimal Price { get; }
}
private readonly struct FractalLine
{
public FractalLine(FractalPoint recent, FractalPoint older)
{
if (recent.Time < older.Time)
{
Recent = older;
Older = recent;
}
else
{
Recent = recent;
Older = older;
}
}
public FractalPoint Recent { get; }
public FractalPoint Older { get; }
public decimal GetPrice(DateTimeOffset time)
{
var totalSeconds = (decimal)(Recent.Time - Older.Time).TotalSeconds;
if (totalSeconds == 0m)
return Recent.Price;
var offsetSeconds = (decimal)(time - Older.Time).TotalSeconds;
return Older.Price + (Recent.Price - Older.Price) * (offsetSeconds / totalSeconds);
}
}
private readonly StrategyParam<bool> _enableSellLine;
private readonly StrategyParam<bool> _enableBuyLine;
private readonly StrategyParam<bool> _enableCloseSellLine;
private readonly StrategyParam<bool> _enableCloseBuyLine;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _lineOffsetSteps;
private readonly StrategyParam<DataType> _candleType;
private decimal _h0;
private decimal _h1;
private decimal _h2;
private decimal _h3;
private decimal _h4;
private decimal _l0;
private decimal _l1;
private decimal _l2;
private decimal _l3;
private decimal _l4;
private DateTimeOffset _t0;
private DateTimeOffset _t1;
private DateTimeOffset _t2;
private DateTimeOffset _t3;
private DateTimeOffset _t4;
private int _bufferCount;
private FractalPoint? _fractal0;
private FractalPoint? _fractal1;
private FractalPoint? _fractal2;
private FractalPoint? _fractal3;
private FractalPoint? _fractal4;
private FractalPoint? _fractal5;
private FractalLine? _sellLine;
private FractalLine? _buyLine;
/// <summary>
/// Enable sell breakout line.
/// </summary>
public bool EnableSellLine
{
get => _enableSellLine.Value;
set => _enableSellLine.Value = value;
}
/// <summary>
/// Enable buy breakout line.
/// </summary>
public bool EnableBuyLine
{
get => _enableBuyLine.Value;
set => _enableBuyLine.Value = value;
}
/// <summary>
/// Enable upper close line (based on sell trend line).
/// </summary>
public bool EnableCloseSellLine
{
get => _enableCloseSellLine.Value;
set => _enableCloseSellLine.Value = value;
}
/// <summary>
/// Enable lower close line (based on buy trend line).
/// </summary>
public bool EnableCloseBuyLine
{
get => _enableCloseBuyLine.Value;
set => _enableCloseBuyLine.Value = value;
}
/// <summary>
/// Maximum number of lots that can remain open.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Order volume per entry.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Offset in price steps for close lines.
/// </summary>
public int LineOffsetSteps
{
get => _lineOffsetSteps.Value;
set => _lineOffsetSteps.Value = value;
}
/// <summary>
/// Candle type used for analysis.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="SendCloseStrategy"/>.
/// </summary>
public SendCloseStrategy()
{
_enableSellLine = Param(nameof(EnableSellLine), true)
.SetDisplay("Sell Line", "Enable sell fractal breakout line", "General");
_enableBuyLine = Param(nameof(EnableBuyLine), true)
.SetDisplay("Buy Line", "Enable buy fractal breakout line", "General");
_enableCloseSellLine = Param(nameof(EnableCloseSellLine), true)
.SetDisplay("Close Line 1", "Enable closing line above sell trend", "General");
_enableCloseBuyLine = Param(nameof(EnableCloseBuyLine), true)
.SetDisplay("Close Line 2", "Enable closing line below buy trend", "General");
_maxPositions = Param(nameof(MaxPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum number of simultaneous lots", "Risk");
_orderVolume = Param(nameof(OrderVolume), 0.10m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Order volume per signal", "Risk");
_lineOffsetSteps = Param(nameof(LineOffsetSteps), 60)
.SetGreaterThanZero()
.SetDisplay("Offset Steps", "Offset in price steps for close levels", "Execution");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Clear buffers that hold recent highs, lows, and times.
_h0 = _h1 = _h2 = _h3 = _h4 = 0m;
_l0 = _l1 = _l2 = _l3 = _l4 = 0m;
_t0 = _t1 = _t2 = _t3 = _t4 = default;
_bufferCount = 0;
// Reset stored fractal points and active lines.
_fractal0 = _fractal1 = _fractal2 = _fractal3 = _fractal4 = _fractal5 = null;
_sellLine = null;
_buyLine = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Subscribe to candle data and process each completed candle.
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Work only with completed candles to match the MT5 expert behaviour.
if (candle.State != CandleStates.Finished)
return;
// Update internal buffers and detect new fractal points.
UpdateBuffers(candle);
UpdateFractalLines();
// Ensure trading is allowed before evaluating signals.
if (!IsFormedAndOnlineAndAllowTrading())
return;
var offset = GetOffset();
var shouldClose = false;
// Check closing logic derived from the upper fractal trend line.
if (EnableCloseSellLine && _sellLine is { } sellLine)
{
var closePrice = GetLinePrice(sellLine, candle.CloseTime) + offset;
if (IsTouched(closePrice, candle))
shouldClose = true;
}
// Check closing logic derived from the lower fractal trend line.
if (EnableCloseBuyLine && _buyLine is { } buyLine)
{
var closePrice = GetLinePrice(buyLine, candle.CloseTime) - offset;
if (IsTouched(closePrice, candle))
shouldClose = true;
}
// Close any open position if price reached one of the close lines.
if (shouldClose && Position != 0m)
{
if (Position > 0) SellMarket(); else BuyMarket();
return;
}
// Entry logic for sell breakout.
if (EnableSellLine && _sellLine is { } sellEntryLine)
{
var sellPrice = GetLinePrice(sellEntryLine, candle.CloseTime);
if (IsTouched(sellPrice, candle))
{
if (Position > 0m)
{
// Flatten long positions before attempting to go short.
SellMarket();
}
else if (CanIncreaseShort())
{
SellMarket(OrderVolume);
}
}
}
// Entry logic for buy breakout.
if (EnableBuyLine && _buyLine is { } buyEntryLine)
{
var buyPrice = GetLinePrice(buyEntryLine, candle.CloseTime);
if (IsTouched(buyPrice, candle))
{
if (Position < 0m)
{
// Flatten short positions before attempting to go long.
BuyMarket();
}
else if (CanIncreaseLong())
{
BuyMarket(OrderVolume);
}
}
}
}
private void UpdateBuffers(ICandleMessage candle)
{
// Shift buffers to keep the latest five candles for fractal detection.
_h4 = _h3;
_h3 = _h2;
_h2 = _h1;
_h1 = _h0;
_h0 = candle.HighPrice;
_l4 = _l3;
_l3 = _l2;
_l2 = _l1;
_l1 = _l0;
_l0 = candle.LowPrice;
_t4 = _t3;
_t3 = _t2;
_t2 = _t1;
_t1 = _t0;
_t0 = candle.OpenTime;
if (_bufferCount < 5)
{
_bufferCount++;
return;
}
// Identify new fractal points once enough candles are available.
if (IsUpFractal())
RegisterFractal(new FractalPoint(FractalTypes.Up, _t2, _h2));
if (IsDownFractal())
RegisterFractal(new FractalPoint(FractalTypes.Down, _t2, _l2));
}
private void UpdateFractalLines()
{
// Build the sell line using the most recent up-down-up pattern.
if (TryBuildLine(FractalTypes.Up, out var sellLine))
_sellLine = sellLine;
// Build the buy line using the most recent down-up-down pattern.
if (TryBuildLine(FractalTypes.Down, out var buyLine))
_buyLine = buyLine;
}
private bool IsUpFractal()
{
return _h2 >= _h3 && _h2 > _h4 && _h2 >= _h1 && _h2 > _h0;
}
private bool IsDownFractal()
{
return _l2 <= _l3 && _l2 < _l4 && _l2 <= _l1 && _l2 < _l0;
}
private void RegisterFractal(FractalPoint point)
{
// Skip duplicates that can appear on flat sequences.
if (_fractal0 is { } latest && latest.Time == point.Time && latest.Type == point.Type)
return;
_fractal5 = _fractal4;
_fractal4 = _fractal3;
_fractal3 = _fractal2;
_fractal2 = _fractal1;
_fractal1 = _fractal0;
_fractal0 = point;
}
private bool TryBuildLine(FractalTypes target, out FractalLine line)
{
line = default;
FractalPoint? latest = null;
FractalPoint? middle = null;
FractalPoint? oldest = null;
foreach (var item in EnumerateFractals())
{
if (item is not { } point)
continue;
if (latest is null)
{
if (point.Type == target)
latest = point;
continue;
}
if (middle is null)
{
if (point.Type != target)
middle = point;
continue;
}
if (point.Type == target)
{
oldest = point;
break;
}
}
if (latest is not { } latestPoint || middle is null || oldest is not { } oldestPoint)
return false;
if (latestPoint.Time == oldestPoint.Time)
return false;
line = new FractalLine(latestPoint, oldestPoint);
return true;
}
private IEnumerable<FractalPoint?> EnumerateFractals()
{
yield return _fractal0;
yield return _fractal1;
yield return _fractal2;
yield return _fractal3;
yield return _fractal4;
yield return _fractal5;
}
private bool CanIncreaseShort()
{
if (OrderVolume <= 0m || MaxPositions <= 0)
return false;
var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
return lots < MaxPositions;
}
private bool CanIncreaseLong()
{
if (OrderVolume <= 0m || MaxPositions <= 0)
return false;
var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
return lots < MaxPositions;
}
private decimal GetOffset()
{
var step = Security?.PriceStep ?? 1m;
return step * LineOffsetSteps;
}
private static bool IsTouched(decimal price, ICandleMessage candle)
{
return price <= candle.HighPrice && price >= candle.LowPrice;
}
private static decimal GetLinePrice(FractalLine line, DateTimeOffset time)
{
return line.GetPrice(time);
}
}
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
from StockSharp.Algo.Strategies import Strategy
UP = 0
DOWN = 1
class send_close_strategy(Strategy):
def __init__(self):
super(send_close_strategy, self).__init__()
self._enable_sell_line = self.Param("EnableSellLine", True)
self._enable_buy_line = self.Param("EnableBuyLine", True)
self._enable_close_sell_line = self.Param("EnableCloseSellLine", True)
self._enable_close_buy_line = self.Param("EnableCloseBuyLine", True)
self._max_positions = self.Param("MaxPositions", 1)
self._order_volume = self.Param("OrderVolume", 0.10)
self._line_offset_steps = self.Param("LineOffsetSteps", 60)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(send_close_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._h = [0.0] * 5
self._l = [0.0] * 5
self._t = [None] * 5
self._buffer_count = 0
self._fractals = [None] * 6
self._sell_line = None
self._buy_line = None
def OnStarted2(self, time):
super(send_close_strategy, self).OnStarted2(time)
self._reset_state()
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_buffers(candle)
self._update_fractal_lines()
if not self.IsFormedAndOnlineAndAllowTrading():
return
offset = self._get_offset()
should_close = False
if self._enable_close_sell_line.Value and self._sell_line is not None:
close_price = self._get_line_price(self._sell_line, candle.CloseTime) + offset
if self._is_touched(close_price, candle):
should_close = True
if self._enable_close_buy_line.Value and self._buy_line is not None:
close_price = self._get_line_price(self._buy_line, candle.CloseTime) - offset
if self._is_touched(close_price, candle):
should_close = True
if should_close and self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
return
if self._enable_sell_line.Value and self._sell_line is not None:
sell_price = self._get_line_price(self._sell_line, candle.CloseTime)
if self._is_touched(sell_price, candle):
if self.Position > 0:
self.SellMarket()
elif self._can_increase_short():
self.SellMarket(float(self._order_volume.Value))
if self._enable_buy_line.Value and self._buy_line is not None:
buy_price = self._get_line_price(self._buy_line, candle.CloseTime)
if self._is_touched(buy_price, candle):
if self.Position < 0:
self.BuyMarket()
elif self._can_increase_long():
self.BuyMarket(float(self._order_volume.Value))
def _update_buffers(self, candle):
h = self._h
l = self._l
t = self._t
h[4] = h[3]; h[3] = h[2]; h[2] = h[1]; h[1] = h[0]; h[0] = float(candle.HighPrice)
l[4] = l[3]; l[3] = l[2]; l[2] = l[1]; l[1] = l[0]; l[0] = float(candle.LowPrice)
t[4] = t[3]; t[3] = t[2]; t[2] = t[1]; t[1] = t[0]; t[0] = candle.OpenTime
if self._buffer_count < 5:
self._buffer_count += 1
return
if self._is_up_fractal():
self._register_fractal(UP, t[2], h[2])
if self._is_down_fractal():
self._register_fractal(DOWN, t[2], l[2])
def _is_up_fractal(self):
h = self._h
return h[2] >= h[3] and h[2] > h[4] and h[2] >= h[1] and h[2] > h[0]
def _is_down_fractal(self):
l = self._l
return l[2] <= l[3] and l[2] < l[4] and l[2] <= l[1] and l[2] < l[0]
def _register_fractal(self, ftype, time, price):
f = self._fractals
if f[0] is not None and f[0][1] == time and f[0][0] == ftype:
return
f[5] = f[4]; f[4] = f[3]; f[3] = f[2]; f[2] = f[1]; f[1] = f[0]
f[0] = (ftype, time, price)
def _update_fractal_lines(self):
sell_line = self._try_build_line(UP)
if sell_line is not None:
self._sell_line = sell_line
buy_line = self._try_build_line(DOWN)
if buy_line is not None:
self._buy_line = buy_line
def _try_build_line(self, target):
latest = None
middle = None
oldest = None
for item in self._fractals:
if item is None:
continue
ftype, ftime, fprice = item
if latest is None:
if ftype == target:
latest = item
continue
if middle is None:
if ftype != target:
middle = item
continue
if ftype == target:
oldest = item
break
if latest is None or middle is None or oldest is None:
return None
_, lt, lp = latest
_, ot, op = oldest
if lt == ot:
return None
if lt < ot:
return (ot, op, lt, lp)
else:
return (lt, lp, ot, op)
def _get_line_price(self, line, time):
recent_time, recent_price, older_time, older_price = line
total_seconds = (recent_time - older_time).TotalSeconds
if total_seconds == 0:
return recent_price
offset_seconds = (time - older_time).TotalSeconds
return older_price + (recent_price - older_price) * (offset_seconds / total_seconds)
def _is_touched(self, price, candle):
return price <= float(candle.HighPrice) and price >= float(candle.LowPrice)
def _can_increase_short(self):
ov = float(self._order_volume.Value)
mp = int(self._max_positions.Value)
if ov <= 0 or mp <= 0:
return False
lots = abs(float(self.Position)) / ov if ov != 0 else 0
return lots < mp
def _can_increase_long(self):
ov = float(self._order_volume.Value)
mp = int(self._max_positions.Value)
if ov <= 0 or mp <= 0:
return False
lots = abs(float(self.Position)) / ov if ov != 0 else 0
return lots < mp
def _get_offset(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
return step * int(self._line_offset_steps.Value)
def CreateClone(self):
return send_close_strategy()