在 GitHub 上查看
Trade Channel
概述
Trade Channel 策略源自 MetaTrader 平台的 “TradeChannel” 智能交易系统。策略利用最近若干根已完成 K 线的最高价与最低价构建价格通道,当通道停止扩张而价格再次触及其边界时,反向开仓,期望价格回到通道内部。
核心思想
- 使用 Highest 和 Lowest 指标构建类似 Donchian Channel 的价格带。
- 只有在通道保持水平(没有新的高点或低点)时才允许入场。
- 价格触及上轨时做空,触及下轨时做多,属于反向交易。
- 初始止损设置在突破点外侧一个 Average True Range (ATR) 的距离。
- 当交易进入盈利区间后,可按需启动移动止损。
参数
| 名称 |
说明 |
默认值 |
优化区间 |
Volume |
下单手数/合约数量。 |
1 |
启用 (0.1 → 2.0,步长 0.1) |
ChannelLength |
计算通道边界所使用的已完成 K 线数量。 |
20 |
启用 (10 → 60,步长 5) |
AtrPeriod |
ATR 指标周期,用于计算初始止损。 |
4 |
启用 (2 → 20,步长 2) |
TrailingPoints |
移动止损偏移量(以合约最小价位为单位),设为 0 表示关闭。 |
30 |
启用 (0 → 100,步长 10) |
CandleType |
参与计算的 K 线类型与周期。 |
30 分钟 K 线 |
— |
交易逻辑
- 订阅参数指定的 K 线,并驱动
Highest、Lowest 和 ATR 三个指标。
- 等待所有指标形成完整数值。首个可用的指标值仅用于初始化通道状态,当根 K 线不进行交易。
- 每当产生新的已完成 K 线:
- 更新通道上轨与下轨,并计算枢轴价
(上轨 + 下轨 + 收盘价) / 3。
- 判断当前上轨/下轨是否与上一根 K 线相同;上轨保持不变时允许做空,下轨保持不变时允许做多。
- 做空信号: 上轨稳定,且 K 线最高价触及上轨,或收盘价位于枢轴价与上轨之间。
- 做多信号: 下轨稳定,且 K 线最低价触及下轨,或收盘价位于下轨与枢轴价之间。
- 策略同一时间仅持有一笔仓位,若已有持仓则忽略新的入场信号。
- 进场后:
- 记录进场价格。
- 做空的止损设置为
上轨 + ATR,做多的止损设置为 下轨 − ATR。
- 持仓管理:
- 多头离场条件:
- 上轨保持水平且价格触及上轨;
- K 线最低价跌破当前(初始或移动)止损价位。
- 空头离场条件:
- 下轨保持水平且价格触及下轨;
- K 线最高价突破当前止损价位。
- 移动止损(当
TrailingPoints > 0 时):
- 利用合约的
Security.Step 将参数转换为真实价格偏移;若该元数据缺失则直接使用参数值。
- 多头在收盘价高于进场价达到偏移量后,将止损提至
收盘价 − 偏移量。
- 空头在收盘价低于进场价达到偏移量后,将止损下移至
收盘价 + 偏移量。
- 移动止损只会收紧保护价,不会回撤。
说明
- 策略仅处理已完成的 K 线,以保持与原始 MQL 版本中
High[1]、Low[1]、Close[1] 的逻辑一致。
- 比较当前与前一条通道边界时,会依据最小价位设置容差,避免浮点误差造成误判。
- 若交易所未提供有效的
Security.Step,移动止损将退化为使用原始点值。
- 原策略中的邮件通知与浮动手数功能依赖 MetaTrader 环境,因此在本转换版本中省略。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trade Channel Breakout strategy - Highest/Lowest channel breakout.
/// Buys when close crosses above channel midpoint.
/// Sells when close crosses below channel midpoint.
/// </summary>
public class TradeChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevMid;
private bool _hasPrev;
public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TradeChannelBreakoutStrategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 20)
.SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevClose = 0m; _prevMid = 0m; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var highest = new Highest { Length = ChannelPeriod };
var lowest = new Lowest { Length = ChannelPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(highest, lowest, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
var mid = (highest + lowest) / 2;
if (!_hasPrev) { _prevClose = close; _prevMid = mid; _hasPrev = true; return; }
if (_prevClose <= _prevMid && close > mid && Position <= 0)
{
if (Position < 0) BuyMarket();
BuyMarket();
}
else if (_prevClose >= _prevMid && close < mid && Position >= 0)
{
if (Position > 0) SellMarket();
SellMarket();
}
_prevClose = close; _prevMid = mid;
}
}
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.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class trade_channel_breakout_strategy(Strategy):
def __init__(self):
super(trade_channel_breakout_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 20).SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_close = 0.0; self._prev_mid = 0.0; self._has_prev = False
@property
def channel_period(self): return self._channel_period.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self):
super(trade_channel_breakout_strategy, self).OnReseted()
self._prev_close = 0.0; self._prev_mid = 0.0; self._has_prev = False
def OnStarted2(self, time):
super(trade_channel_breakout_strategy, self).OnStarted2(time)
self._has_prev = False
highest = Highest(); highest.Length = self.channel_period
lowest = Lowest(); lowest.Length = self.channel_period
sub = self.SubscribeCandles(self.candle_type)
sub.Bind(highest, lowest, self.process_candle).Start()
def process_candle(self, candle, highest, lowest):
if candle.State != CandleStates.Finished: return
close = float(candle.ClosePrice); mid = (float(highest) + float(lowest)) / 2.0
if not self._has_prev: self._prev_close = close; self._prev_mid = mid; self._has_prev = True; return
if self._prev_close <= self._prev_mid and close > mid and self.Position <= 0:
if self.Position < 0: self.BuyMarket()
self.BuyMarket()
elif self._prev_close >= self._prev_mid and close < mid and self.Position >= 0:
if self.Position > 0: self.SellMarket()
self.SellMarket()
self._prev_close = close; self._prev_mid = mid
def CreateClone(self): return trade_channel_breakout_strategy()