箭头与曲线策略
概述
该策略是 MetaTrader 5 "Arrows and Curves" 专家顾问的 C# 版本。策略在 StockSharp 高阶 API 中复现了原始指标驱动的交易逻辑,只针对单一标的进行操作。每次只有一笔持仓处于激活状态,新的信号会在没有仓位时开仓,或在已有仓位时执行平仓。
策略逻辑
- 通过
SubscribeCandles订阅所选时间框的K线,并且只处理已完成的K线,这与原始 EA 仅在新柱出现时运作的方式一致。 - 在策略内部重建 Arrows and Curves 通道:算法在
SSP回溯窗口内(向后偏移Relay根柱)查找最高价和最低价,再根据Channel %与Channel Stop %计算出外层与内层带状线。 - 指标状态变量
uptrend和uptrend2的更新顺序完全按照 MQL 代码执行。上一根K线出现 Sell 箭头时,策略为多头做好准备;出现 Buy 箭头时,则为空头做好准备。这与原始 EA 在下一根柱读取索引为1的缓冲区值的逻辑一致。 - 当没有持仓时,使用上一根K线保存的信号开市价单,并且方向与箭头相反(Sell 箭头→买入,Buy 箭头→卖出)。
- 当已经有持仓时,若出现反向信号则先行平仓,但不会立刻反向开仓——这与 MT5 源码一致:先退出,再在下一根K线上重新评估入场。
风险管理
- 止损与止盈以“点”作为单位,并通过
PriceStep转换为绝对价格距离。如果品种报价有3位或5位小数,则会将最小价位变动乘以10,以复刻 EA 中的点值调整方法。 - 拖尾止损遵循 EA 的实现:当浮动盈利超过
Trailing Stop + Trailing Step时,保护性止损会按设定的距离跟随,并遵守最小移动步长。 - 每根完成的K线都会使用当根的最高价/最低价来近似检测止损或止盈是否触发,以模拟盘中触发效果。
Volume参数提供固定下单数量;当其设为0时,策略会按照Risk %将账户价值的一定比例暴露在配置的止损距离之内,从而得到动态下单数量。
参数
Volume:固定下单数量;为0时启用风险百分比头寸管理。Risk %:在启用动态仓位时用于计算风险的账户百分比。Stop Loss (pips):以点数表示的止损距离。Take Profit (pips):以点数表示的止盈距离。Trailing Stop (pips):拖尾止损距离,为0时禁用。Trailing Step (pips):拖尾重新移动之前所需的附加点数。SSP:计算通道范围的回溯K线数量。Channel %:外层带状线百分比,完全对应原始 MT5 设置。Channel Stop %:控制内部状态翻转的内层带状线百分比。Relay:通道计算中使用的偏移量。Candle Type:参与计算的时间框或K线类型。
实现说明
- 策略仅保留计算指标所需的最小历史数据(
SSP + Relay + 5根K线)。 - 代码中的注释和辅助函数全部使用英文,以满足仓库约定。
- 与 MT5 不同,止损和止盈在此实现中通过K线的高低价模拟,因此盘中触发可能与原始 EA 略有差异;除这一点外,其余决策逻辑与原始脚本保持一致。
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>
/// Port of the MT5 Arrows and Curves expert advisor using StockSharp high level API.
/// </summary>
public class ArrowsAndCurvesStrategy : Strategy
{
private readonly StrategyParam<decimal> _volume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _sspPeriod;
private readonly StrategyParam<int> _channelPercent;
private readonly StrategyParam<int> _channelStopPercent;
private readonly StrategyParam<int> _relayShift;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highSeries = new();
private readonly List<decimal> _lowSeries = new();
private readonly List<decimal> _closeSeries = new();
private bool _uptrend;
private bool _uptrend2;
private bool _previousSellArrow;
private bool _previousBuyArrow;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
public decimal VolumeValue { get => _volume.Value; set => _volume.Value = value; }
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
public int SspPeriod { get => _sspPeriod.Value; set => _sspPeriod.Value = value; }
public int ChannelPercent { get => _channelPercent.Value; set => _channelPercent.Value = value; }
public int ChannelStopPercent { get => _channelStopPercent.Value; set => _channelStopPercent.Value = value; }
public int RelayShift { get => _relayShift.Value; set => _relayShift.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public ArrowsAndCurvesStrategy()
{
_volume = Param(nameof(VolumeValue), 1m)
.SetNotNegative()
.SetDisplay("Volume", "Order volume", "Trading");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetNotNegative()
.SetDisplay("Risk %", "Risk percent for dynamic sizing when volume is zero", "Trading");
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 0)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Minimum movement before trailing updates", "Risk");
_sspPeriod = Param(nameof(SspPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("SSP", "Lookback period of the custom channel", "Indicator");
_channelPercent = Param(nameof(ChannelPercent), 0)
.SetNotNegative()
.SetDisplay("Channel %", "Outer channel percentage", "Indicator");
_channelStopPercent = Param(nameof(ChannelStopPercent), 30)
.SetNotNegative()
.SetDisplay("Channel Stop %", "Inner channel percentage", "Indicator");
_relayShift = Param(nameof(RelayShift), 10)
.SetNotNegative()
.SetDisplay("Relay", "Shift used by the indicator", "Indicator");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candles used for processing", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_highSeries.Clear();
_lowSeries.Clear();
_closeSeries.Clear();
_uptrend = false;
_uptrend2 = false;
_previousSellArrow = false;
_previousBuyArrow = false;
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
AddCandle(candle);
var shouldOpenBuy = _previousSellArrow;
var shouldOpenSell = _previousBuyArrow;
if (Position == 0)
{
if (shouldOpenBuy)
OpenLong(candle);
else if (shouldOpenSell)
OpenShort(candle);
}
else
{
if (Position > 0 && shouldOpenSell)
{
CloseAndReset();
}
else if (Position < 0 && shouldOpenBuy)
{
CloseAndReset();
}
UpdateTrailing(candle);
CheckRiskExits(candle);
}
if (!TryComputeSignals(out var buySignal, out var sellSignal))
{
_previousBuyArrow = false;
_previousSellArrow = false;
return;
}
_previousBuyArrow = buySignal;
_previousSellArrow = sellSignal;
}
private void OpenLong(ICandleMessage candle)
{
var volume = GetOrderVolume(candle.ClosePrice);
if (volume <= 0m)
return;
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = StopLossPips > 0 ? candle.ClosePrice - ConvertPips(StopLossPips) : null;
_takePrice = TakeProfitPips > 0 ? candle.ClosePrice + ConvertPips(TakeProfitPips) : null;
}
private void OpenShort(ICandleMessage candle)
{
var volume = GetOrderVolume(candle.ClosePrice);
if (volume <= 0m)
return;
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = StopLossPips > 0 ? candle.ClosePrice + ConvertPips(StopLossPips) : null;
_takePrice = TakeProfitPips > 0 ? candle.ClosePrice - ConvertPips(TakeProfitPips) : null;
}
private void UpdateTrailing(ICandleMessage candle)
{
if (TrailingStopPips <= 0 || _entryPrice == null)
return;
var distance = ConvertPips(TrailingStopPips);
if (distance <= 0m)
return;
var step = ConvertPips(TrailingStepPips);
if (Position > 0)
{
var gain = candle.ClosePrice - _entryPrice.Value;
if (gain > distance + step)
{
var newStop = candle.ClosePrice - distance;
if (!_stopPrice.HasValue || _stopPrice.Value < newStop - step)
_stopPrice = newStop;
}
}
else if (Position < 0)
{
var gain = _entryPrice.Value - candle.ClosePrice;
if (gain > distance + step)
{
var newStop = candle.ClosePrice + distance;
if (!_stopPrice.HasValue || _stopPrice.Value > newStop + step)
_stopPrice = newStop;
}
}
}
private void CheckRiskExits(ICandleMessage candle)
{
if (Position > 0)
{
var stopHit = _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value;
var takeHit = _takePrice.HasValue && candle.HighPrice >= _takePrice.Value;
if (stopHit || takeHit)
CloseAndReset();
}
else if (Position < 0)
{
var stopHit = _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value;
var takeHit = _takePrice.HasValue && candle.LowPrice <= _takePrice.Value;
if (stopHit || takeHit)
CloseAndReset();
}
}
private void CloseAndReset()
{
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(-Position);
ResetPositionState();
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
private void AddCandle(ICandleMessage candle)
{
_highSeries.Add(candle.HighPrice);
_lowSeries.Add(candle.LowPrice);
_closeSeries.Add(candle.ClosePrice);
var maxCount = RelayShift + SspPeriod + 5;
TrimSeries(_highSeries, maxCount);
TrimSeries(_lowSeries, maxCount);
TrimSeries(_closeSeries, maxCount);
}
private static void TrimSeries(List<decimal> series, int maxCount)
{
var excess = series.Count - maxCount;
if (excess > 0)
series.RemoveRange(0, excess);
}
private bool TryComputeSignals(out bool buySignal, out bool sellSignal)
{
buySignal = false;
sellSignal = false;
if (_closeSeries.Count <= 1)
return false;
var start = RelayShift + 1;
var end = start + SspPeriod;
if (end > _highSeries.Count || end > _lowSeries.Count)
return false;
var close = GetSeriesValue(_closeSeries, 1);
decimal high = decimal.MinValue;
decimal low = decimal.MaxValue;
for (var i = start; i < end; i++)
{
var h = GetSeriesValue(_highSeries, i);
var l = GetSeriesValue(_lowSeries, i);
if (h > high)
high = h;
if (l < low)
low = l;
}
var range = high - low;
var smax = high - (low - high) * ChannelPercent / 100m;
var smin = low + range * ChannelPercent / 100m;
var innerPercent = ChannelPercent + ChannelStopPercent;
var smax2 = high - range * innerPercent / 100m;
var smin2 = low + range * innerPercent / 100m;
var uptrend = _uptrend;
var uptrend2 = _uptrend2;
var old = uptrend;
var old2 = uptrend2;
if (close < smin && close < smax && uptrend2)
uptrend = false;
if (close > smax && close > smin && !uptrend2)
uptrend = true;
if ((close > smax2 || close > smin2) && !uptrend)
uptrend2 = false;
if ((close < smin2 || close < smax2) && uptrend)
uptrend2 = true;
if (close < smin && close < smax && !uptrend2)
{
sellSignal = true;
uptrend2 = true;
}
if (close > smax && close > smin && uptrend2)
{
buySignal = true;
uptrend2 = false;
}
if (uptrend != old && !uptrend)
sellSignal = true;
if (uptrend != old && uptrend)
buySignal = true;
_uptrend = uptrend;
_uptrend2 = uptrend2;
return true;
}
private static decimal GetSeriesValue(List<decimal> series, int index)
{
var targetIndex = series.Count - 1 - index;
return targetIndex >= 0 ? series[targetIndex] : 0m;
}
private decimal GetOrderVolume(decimal price)
{
if (VolumeValue > 0m)
return VolumeValue;
var portfolio = Portfolio;
var portfolioValue = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
if (portfolioValue <= 0m || RiskPercent <= 0m)
return 1m;
var riskAmount = portfolioValue * RiskPercent / 100m;
var stopOffset = StopLossPips > 0 ? ConvertPips(StopLossPips) : price * 0.01m;
if (stopOffset <= 0m)
return 1m;
var volume = riskAmount / stopOffset;
return volume > 0m ? volume : 1m;
}
private decimal ConvertPips(int pips)
{
if (pips <= 0)
return 0m;
var pipSize = GetPipSize();
return pipSize <= 0m ? 0m : pipSize * pips;
}
private decimal GetPipSize()
{
var security = Security;
if (security == null)
return 0m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var scale = GetDecimalScale(step);
var factor = scale is 3 or 5 ? 10m : 1m;
return step * factor;
}
private static int GetDecimalScale(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, Math
from StockSharp.Messages import CandleStates
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class arrows_and_curves_strategy(Strategy):
"""
Port of Arrows and Curves EA. Uses custom channel computation for signals
with SL/TP and trailing stop.
"""
def __init__(self):
super(arrows_and_curves_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 5) \
.SetDisplay("Trailing Step (pips)", "Minimum movement before trailing updates", "Risk")
self._ssp_period = self.Param("SspPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("SSP", "Lookback period of the custom channel", "Indicator")
self._channel_percent = self.Param("ChannelPercent", 0) \
.SetDisplay("Channel %", "Outer channel percentage", "Indicator")
self._channel_stop_percent = self.Param("ChannelStopPercent", 30) \
.SetDisplay("Channel Stop %", "Inner channel percentage", "Indicator")
self._relay_shift = self.Param("RelayShift", 10) \
.SetDisplay("Relay", "Shift used by the indicator", "Indicator")
self._candle_type = self.Param("CandleType", tf(15)) \
.SetDisplay("Candle Type", "Candles used for processing", "General")
self._high_series = []
self._low_series = []
self._close_series = []
self._uptrend = False
self._uptrend2 = False
self._previous_sell_arrow = False
self._previous_buy_arrow = False
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def StopLossPips(self): return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, v): self._stop_loss_pips.Value = v
@property
def TakeProfitPips(self): return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, v): self._take_profit_pips.Value = v
@property
def TrailingStopPips(self): return self._trailing_stop_pips.Value
@TrailingStopPips.setter
def TrailingStopPips(self, v): self._trailing_stop_pips.Value = v
@property
def TrailingStepPips(self): return self._trailing_step_pips.Value
@TrailingStepPips.setter
def TrailingStepPips(self, v): self._trailing_step_pips.Value = v
@property
def SspPeriod(self): return self._ssp_period.Value
@SspPeriod.setter
def SspPeriod(self, v): self._ssp_period.Value = v
@property
def ChannelPercent(self): return self._channel_percent.Value
@ChannelPercent.setter
def ChannelPercent(self, v): self._channel_percent.Value = v
@property
def ChannelStopPercent(self): return self._channel_stop_percent.Value
@ChannelStopPercent.setter
def ChannelStopPercent(self, v): self._channel_stop_percent.Value = v
@property
def RelayShift(self): return self._relay_shift.Value
@RelayShift.setter
def RelayShift(self, v): self._relay_shift.Value = v
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, v): self._candle_type.Value = v
def OnReseted(self):
super(arrows_and_curves_strategy, self).OnReseted()
self._high_series = []
self._low_series = []
self._close_series = []
self._uptrend = False
self._uptrend2 = False
self._previous_sell_arrow = False
self._previous_buy_arrow = False
self._entry_price = None
self._stop_price = None
self._take_price = None
def OnStarted2(self, time):
super(arrows_and_curves_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._add_candle(candle)
should_open_buy = self._previous_sell_arrow
should_open_sell = self._previous_buy_arrow
if self.Position == 0:
if should_open_buy:
self._open_long(candle)
elif should_open_sell:
self._open_short(candle)
else:
if self.Position > 0 and should_open_sell:
self._close_and_reset()
elif self.Position < 0 and should_open_buy:
self._close_and_reset()
self._update_trailing(candle)
self._check_risk_exits(candle)
result = self._try_compute_signals()
if result is None:
self._previous_buy_arrow = False
self._previous_sell_arrow = False
else:
self._previous_buy_arrow = result[0]
self._previous_sell_arrow = result[1]
def _open_long(self, candle):
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep or 0.01)
self.BuyMarket(self.Volume)
self._entry_price = close
self._stop_price = close - self.StopLossPips * step if self.StopLossPips > 0 else None
self._take_price = close + self.TakeProfitPips * step if self.TakeProfitPips > 0 else None
def _open_short(self, candle):
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep or 0.01)
self.SellMarket(self.Volume)
self._entry_price = close
self._stop_price = close + self.StopLossPips * step if self.StopLossPips > 0 else None
self._take_price = close - self.TakeProfitPips * step if self.TakeProfitPips > 0 else None
def _update_trailing(self, candle):
if self.TrailingStopPips <= 0 or self._entry_price is None:
return
step = float(self.Security.PriceStep or 0.01)
distance = self.TrailingStopPips * step
if distance <= 0:
return
trail_step = self.TrailingStepPips * step
close = float(candle.ClosePrice)
if self.Position > 0:
gain = close - self._entry_price
if gain > distance + trail_step:
new_stop = close - distance
if self._stop_price is None or self._stop_price < new_stop - trail_step:
self._stop_price = new_stop
elif self.Position < 0:
gain = self._entry_price - close
if gain > distance + trail_step:
new_stop = close + distance
if self._stop_price is None or self._stop_price > new_stop + trail_step:
self._stop_price = new_stop
def _check_risk_exits(self, candle):
if self.Position > 0:
stop_hit = self._stop_price is not None and float(candle.LowPrice) <= self._stop_price
take_hit = self._take_price is not None and float(candle.HighPrice) >= self._take_price
if stop_hit or take_hit:
self._close_and_reset()
elif self.Position < 0:
stop_hit = self._stop_price is not None and float(candle.HighPrice) >= self._stop_price
take_hit = self._take_price is not None and float(candle.LowPrice) <= self._take_price
if stop_hit or take_hit:
self._close_and_reset()
def _close_and_reset(self):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self._entry_price = None
self._stop_price = None
self._take_price = None
def _add_candle(self, candle):
self._high_series.append(float(candle.HighPrice))
self._low_series.append(float(candle.LowPrice))
self._close_series.append(float(candle.ClosePrice))
max_count = self.RelayShift + self.SspPeriod + 5
if len(self._high_series) > max_count:
excess = len(self._high_series) - max_count
self._high_series = self._high_series[excess:]
self._low_series = self._low_series[excess:]
self._close_series = self._close_series[excess:]
def _get_series_value(self, series, index):
target = len(series) - 1 - index
return series[target] if target >= 0 else 0.0
def _try_compute_signals(self):
if len(self._close_series) <= 1:
return None
start = self.RelayShift + 1
end = start + self.SspPeriod
if end > len(self._high_series) or end > len(self._low_series):
return None
close = self._get_series_value(self._close_series, 1)
high = float('-inf')
low = float('inf')
for i in range(start, end):
h = self._get_series_value(self._high_series, i)
l = self._get_series_value(self._low_series, i)
if h > high:
high = h
if l < low:
low = l
rng = high - low
smax = high - (low - high) * self.ChannelPercent / 100.0
smin = low + rng * self.ChannelPercent / 100.0
inner_percent = self.ChannelPercent + self.ChannelStopPercent
smax2 = high - rng * inner_percent / 100.0
smin2 = low + rng * inner_percent / 100.0
uptrend = self._uptrend
uptrend2 = self._uptrend2
old = uptrend
old2 = uptrend2
buy_signal = False
sell_signal = False
if close < smin and close < smax and uptrend2:
uptrend = False
if close > smax and close > smin and not uptrend2:
uptrend = True
if (close > smax2 or close > smin2) and not uptrend:
uptrend2 = False
if (close < smin2 or close < smax2) and uptrend:
uptrend2 = True
if close < smin and close < smax and not uptrend2:
sell_signal = True
uptrend2 = True
if close > smax and close > smin and uptrend2:
buy_signal = True
uptrend2 = False
if uptrend != old and not uptrend:
sell_signal = True
if uptrend != old and uptrend:
buy_signal = True
self._uptrend = uptrend
self._uptrend2 = uptrend2
return (buy_signal, sell_signal)
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return arrows_and_curves_strategy()