Currencyprofits High-Low Channel 策略
概览
该策略是 MetaTrader 专家顾问 Currencyprofits_01.1 的 StockSharp 版本。策略通过一快一慢两条移动平均线确定趋势方向,并在价格回测最近通道的极值时入场。快线位于慢线之上时判定为多头环境,等待价格回到最近若干根 K 线的最低点;快线位于慢线之下时判定为空头环境,关注价格回测通道最高点以入场做空。
所有计算均基于收盘完成的 K 线执行,可同时用于回测和实时交易。
交易逻辑
- 订阅指定的蜡烛类型,计算快/慢两条移动平均线以及由最近
ChannelLength根 K 线构成的高低通道(默认 6 根)。 - 保存上一根 K 线的指标值,以复刻原版 MQL 程序中使用的 1 根偏移逻辑。
- 做多条件:上一根快均线大于慢均线,且当前 K 线最低价触及或突破上一周期通道最低值。
- 做空条件:上一根快均线小于慢均线,且当前 K 线最高价触及或突破上一周期通道最高值。
- 离场规则:
- 多头仓位在下一根 K 线收盘价高于保存的通道最高值时平仓,或触发止损价位时平仓。
- 空头仓位在下一根 K 线收盘价低于保存的通道最低值时平仓,或触发止损价位时平仓。
- 策略同一时间只持有一个方向的仓位,持仓期间新的信号不会执行。
风险控制
RiskPercent表示单笔交易可承担的账户资金比例(默认0.14,即 14%)。- 止损距离由
StopLossPoints与标的物的PriceStep相乘得到;若没有步长信息则直接使用点数。 - 单合约的资金风险通过交易所步值
StepPrice估算;如果该数据缺失,则退化为价格差值。 - 最终委托量会按照
VolumeStep、MinVolume、MaxVolume等交易规则对齐。若无法计算风险仓位,则使用策略的默认Volume。
参数
FastLength—— 快速移动平均线周期(默认 32)。FastMaType—— 快速移动平均线类型(Simple、Exponential、Smoothed、Weighted)。SlowLength—— 慢速移动平均线周期(默认 86)。SlowMaType—— 慢速移动平均线类型。PriceSource—— 参与移动平均计算的蜡烛价格(默认收盘价)。ChannelLength—— 计算高低通道时使用的历史 K 线数量(默认 6)。StopLossPoints—— 止损距离(点数表示,默认 170)。RiskPercent—— 单笔交易的风险资金比例(默认 0.14 → 14%)。CandleType—— 用于计算的蜡烛类型/周期(默认 1 小时,可按需求调整)。
使用建议
- 启动前请填写
PriceStep、StepPrice以及相关成交量约束,以确保仓位计算准确。 - 当
RiskPercent = 0或不希望动态计算仓位时,请为策略设置合适的Volume作为回退值。 - 策略在确认的收盘 K 线后执行交易。
- 该实现不设独立的止盈,与原始专家顾问一致,通过止损和通道反向突破退出。
来源
基于 MQL/17641/Currencyprofits_01.1.mq5 转换,并使用 StockSharp 高阶 API 实现。
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>
/// Currencyprofits strategy that trades trend pullbacks into the recent channel extremes.
/// </summary>
public class CurrencyprofitsHighLowChannelStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _channelLength;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<CandlePrices> _priceSource;
private readonly StrategyParam<MovingAverageTypes> _fastMaType;
private readonly StrategyParam<MovingAverageTypes> _slowMaType;
private readonly StrategyParam<int> _signalCooldownBars;
private decimal? _previousFast;
private decimal? _previousSlow;
private decimal? _previousHighest;
private decimal? _previousLowest;
private decimal _entryPrice;
private decimal _stopPrice;
private int _processedCandles;
private int _cooldownRemaining;
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
public int ChannelLength
{
get => _channelLength.Value;
set => _channelLength.Value = value;
}
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public CandlePrices PriceSource
{
get => _priceSource.Value;
set => _priceSource.Value = value;
}
public MovingAverageTypes FastMaType
{
get => _fastMaType.Value;
set => _fastMaType.Value = value;
}
public MovingAverageTypes SlowMaType
{
get => _slowMaType.Value;
set => _slowMaType.Value = value;
}
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
private int RequiredBars => Math.Max(Math.Max(FastLength, SlowLength), ChannelLength) + 1;
public CurrencyprofitsHighLowChannelStrategy()
{
_fastLength = Param(nameof(FastLength), 32)
.SetDisplay("Fast MA Length", "Length of the fast moving average", "Indicators")
.SetOptimize(10, 120, 2);
_slowLength = Param(nameof(SlowLength), 86)
.SetDisplay("Slow MA Length", "Length of the slow moving average", "Indicators")
.SetOptimize(20, 200, 2);
_channelLength = Param(nameof(ChannelLength), 12)
.SetDisplay("Channel Lookback", "Number of previous candles for high/low channel", "Indicators")
.SetOptimize(3, 20, 1);
_stopLossPoints = Param(nameof(StopLossPoints), 170m)
.SetDisplay("Stop Loss (points)", "Distance to stop loss expressed in price steps", "Risk");
_riskPercent = Param(nameof(RiskPercent), 0.14m)
.SetDisplay("Risk Fraction", "Fraction of portfolio capital risked per trade", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");
_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
.SetDisplay("MA Price Source", "Price source used by both moving averages", "Indicators");
_fastMaType = Param(nameof(FastMaType), MovingAverageTypes.Simple)
.SetDisplay("Fast MA Type", "Moving average algorithm for the fast line", "Indicators");
_slowMaType = Param(nameof(SlowMaType), MovingAverageTypes.Simple)
.SetDisplay("Slow MA Type", "Moving average algorithm for the slow line", "Indicators");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before the next entry", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousFast = null;
_previousSlow = null;
_previousHighest = null;
_previousLowest = null;
_entryPrice = 0m;
_stopPrice = 0m;
_processedCandles = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastMa = CreateMovingAverage(FastMaType, FastLength, PriceSource);
var slowMa = CreateMovingAverage(SlowMaType, SlowLength, PriceSource);
var highest = new Highest { Length = ChannelLength };
var lowest = new Lowest { Length = ChannelLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(fastMa, slowMa, highest, lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal channelHigh, decimal channelLow)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
_processedCandles++;
if (_processedCandles <= RequiredBars)
{
// Collect enough history before taking any decisions.
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
return;
}
if (_previousFast is null || _previousSlow is null || _previousHighest is null || _previousLowest is null)
{
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
return;
}
if (Position > 0)
{
// Exit long trades when price breaks the opposite channel or the protective stop.
var exitByChannel = candle.ClosePrice >= _previousHighest.Value;
var exitByStop = _stopPrice > 0m && candle.LowPrice <= _stopPrice;
if (exitByChannel || exitByStop)
{
SellMarket(Position);
ResetTradeState();
_cooldownRemaining = SignalCooldownBars;
}
}
else if (Position < 0)
{
// Exit short trades when price hits the lower boundary or the stop.
var exitByChannel = candle.ClosePrice <= _previousLowest.Value;
var exitByStop = _stopPrice > 0m && candle.HighPrice >= _stopPrice;
if (exitByChannel || exitByStop)
{
BuyMarket(-Position);
ResetTradeState();
_cooldownRemaining = SignalCooldownBars;
}
}
else if (_cooldownRemaining == 0)
{
var stopDistance = GetStopDistance();
if (stopDistance > 0m)
{
var bullishTrend = _previousFast.Value > _previousSlow.Value && fast > slow;
var bearishTrend = _previousFast.Value < _previousSlow.Value && fast < slow;
var bullishReversal = candle.LowPrice <= _previousLowest.Value && candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > fast;
var bearishReversal = candle.HighPrice >= _previousHighest.Value && candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < fast;
// Long entries require a bullish trend and a pullback to the recent low channel.
if (bullishTrend && bullishReversal)
{
var volume = CalculatePositionSize(stopDistance);
if (volume > 0m)
{
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - stopDistance;
_cooldownRemaining = SignalCooldownBars;
}
}
// Short entries require a bearish trend and a retest of the recent high channel.
else if (bearishTrend && bearishReversal)
{
var volume = CalculatePositionSize(stopDistance);
if (volume > 0m)
{
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + stopDistance;
_cooldownRemaining = SignalCooldownBars;
}
}
}
}
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
}
private decimal GetStopDistance()
{
if (StopLossPoints <= 0m)
return 0m;
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep > 0m)
return StopLossPoints * priceStep;
return StopLossPoints;
}
private decimal CalculatePositionSize(decimal stopDistance)
{
var defaultVolume = AdjustVolume(Volume);
if (stopDistance <= 0m)
return defaultVolume;
var portfolioValue = Portfolio?.CurrentValue;
if (portfolioValue is null || portfolioValue <= 0m || RiskPercent <= 0m)
return defaultVolume;
var riskCapital = portfolioValue.Value * RiskPercent;
var priceStep = Security?.PriceStep ?? 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
decimal riskPerContract;
if (priceStep > 0m && stepPrice > 0m)
{
// Convert the stop distance into cash risk per contract using exchange specifications.
riskPerContract = stopDistance / priceStep * stepPrice;
}
else
{
// Fallback when the security does not expose step metadata.
riskPerContract = stopDistance;
}
if (riskPerContract <= 0m)
return defaultVolume;
var desiredVolume = riskCapital / riskPerContract;
return AdjustVolume(desiredVolume);
}
private decimal AdjustVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = decimal.Floor(volume / step);
volume = steps * step;
}
var minVolume = Security?.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = Security?.MaxVolume ?? 0m;
if (maxVolume > 0m && volume > maxVolume)
volume = maxVolume;
return volume;
}
private void ResetTradeState()
{
// Clear cached execution details after a position has been closed.
_entryPrice = 0m;
_stopPrice = 0m;
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices price)
{
return type switch
{
MovingAverageTypes.Simple => new SMA { Length = length },
MovingAverageTypes.Exponential => new EMA { Length = length },
MovingAverageTypes.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageTypes.Weighted => new WeightedMovingAverage { Length = length },
_ => new SMA { Length = length },
};
}
public enum MovingAverageTypes
{
Simple,
Exponential,
Smoothed,
Weighted,
}
public enum CandlePrices
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import (SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage, Highest, Lowest)
from StockSharp.Algo.Strategies import Strategy
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_SMOOTHED = 2
MA_WEIGHTED = 3
class currencyprofits_high_low_channel_strategy(Strategy):
def __init__(self):
super(currencyprofits_high_low_channel_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 32)
self._slow_length = self.Param("SlowLength", 86)
self._channel_length = self.Param("ChannelLength", 12)
self._stop_loss_points = self.Param("StopLossPoints", 170.0)
self._risk_percent = self.Param("RiskPercent", 0.14)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._fast_ma_type = self.Param("FastMaType", MA_SIMPLE)
self._slow_ma_type = self.Param("SlowMaType", MA_SIMPLE)
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4)
self._previous_fast = None
self._previous_slow = None
self._previous_highest = None
self._previous_lowest = None
self._entry_price = 0.0
self._stop_price = 0.0
self._processed_candles = 0
self._cooldown_remaining = 0
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.Value = value
@property
def ChannelLength(self):
return self._channel_length.Value
@ChannelLength.setter
def ChannelLength(self, value):
self._channel_length.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMaType(self):
return self._fast_ma_type.Value
@FastMaType.setter
def FastMaType(self, value):
self._fast_ma_type.Value = value
@property
def SlowMaType(self):
return self._slow_ma_type.Value
@SlowMaType.setter
def SlowMaType(self, value):
self._slow_ma_type.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
def _create_ma(self, ma_type, length):
t = int(ma_type)
if t == MA_EXPONENTIAL:
ind = ExponentialMovingAverage()
ind.Length = length
return ind
elif t == MA_SMOOTHED:
ind = SmoothedMovingAverage()
ind.Length = length
return ind
elif t == MA_WEIGHTED:
ind = WeightedMovingAverage()
ind.Length = length
return ind
else:
ind = SimpleMovingAverage()
ind.Length = length
return ind
def OnStarted2(self, time):
super(currencyprofits_high_low_channel_strategy, self).OnStarted2(time)
fast_ma = self._create_ma(self.FastMaType, self.FastLength)
slow_ma = self._create_ma(self.SlowMaType, self.SlowLength)
highest = Highest()
highest.Length = self.ChannelLength
lowest = Lowest()
lowest.Length = self.ChannelLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ma, slow_ma, highest, lowest, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, fast, slow, channel_high, channel_low):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._processed_candles += 1
fast_val = float(fast)
slow_val = float(slow)
ch_high = float(channel_high)
ch_low = float(channel_low)
close = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
required = max(max(int(self.FastLength), int(self.SlowLength)), int(self.ChannelLength)) + 1
if self._processed_candles <= required:
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
return
if self._previous_fast is None or self._previous_slow is None or self._previous_highest is None or self._previous_lowest is None:
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
return
if self.Position > 0:
exit_by_channel = close >= self._previous_highest
exit_by_stop = self._stop_price > 0.0 and low <= self._stop_price
if exit_by_channel or exit_by_stop:
self.SellMarket()
self._reset_trade_state()
self._cooldown_remaining = int(self.SignalCooldownBars)
elif self.Position < 0:
exit_by_channel = close <= self._previous_lowest
exit_by_stop = self._stop_price > 0.0 and high >= self._stop_price
if exit_by_channel or exit_by_stop:
self.BuyMarket()
self._reset_trade_state()
self._cooldown_remaining = int(self.SignalCooldownBars)
elif self._cooldown_remaining == 0:
stop_distance = self._get_stop_distance()
if stop_distance > 0.0:
bullish_trend = self._previous_fast > self._previous_slow and fast_val > slow_val
bearish_trend = self._previous_fast < self._previous_slow and fast_val < slow_val
bullish_reversal = low <= self._previous_lowest and close > open_price and close > fast_val
bearish_reversal = high >= self._previous_highest and close < open_price and close < fast_val
if bullish_trend and bullish_reversal:
self.BuyMarket()
self._entry_price = close
self._stop_price = self._entry_price - stop_distance
self._cooldown_remaining = int(self.SignalCooldownBars)
elif bearish_trend and bearish_reversal:
self.SellMarket()
self._entry_price = close
self._stop_price = self._entry_price + stop_distance
self._cooldown_remaining = int(self.SignalCooldownBars)
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
def _get_stop_distance(self):
sl = float(self.StopLossPoints)
if sl <= 0.0:
return 0.0
ps = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if ps > 0.0:
return sl * ps
return sl
def _reset_trade_state(self):
self._entry_price = 0.0
self._stop_price = 0.0
def OnReseted(self):
super(currencyprofits_high_low_channel_strategy, self).OnReseted()
self._previous_fast = None
self._previous_slow = None
self._previous_highest = None
self._previous_lowest = None
self._entry_price = 0.0
self._stop_price = 0.0
self._processed_candles = 0
self._cooldown_remaining = 0
def CreateClone(self):
return currencyprofits_high_low_channel_strategy()