在 GitHub 上查看

Scalpel 策略

概述

Scalpel 策略 是 MetaTrader 4 智能交易系统 Scalpel.mq4 的 StockSharp 版本。策略在基础周期上寻找突破行情,结合 H4/H1/M30 更高周期的高低点来确认趋势,同时利用 1 分钟波动周期的方向性成交量进行过滤。持仓管理完全复刻原版 EA:固定止盈会随着时间缩短,价格走出足够利润后启用跟踪止损,并可以在设置的持仓时长或周五晚间强制平仓。

交易逻辑

  • 多周期趋势过滤:多头信号要求当前 H4、H1、M30 K 线的最低价分别高于前一根。空头信号则要求最高价逐级下降。
  • 突破确认:基础周期内,当最新卖价突破前一根 K 线的最高价(做多)或最新买价跌破前一根最低价(做空)时触发,同时最近三根 K 线的高点/低点必须呈阶梯形排列。
  • CCI 区间:使用上一根已完成 K 线的商品通道指数,信号必须位于零附近的可配置区间内。正值表示对称区间,负值按照原始 EA 的方式为其中一侧放宽限制。
  • 方向性成交量过滤:在波动周期中将成交量划分为两个滚动窗口。只有当最新窗口的方向性成交量超过旧窗口且旧窗口非零时才允许交易。若 VolatilityWindow 为负,则改为基于 K 线振幅的无方向累加。
  • 风险控制
    • 止盈止损距离以最小价格步长表示。
    • 每经过 TakeProfitReduceMinutes 分钟,止盈价向持仓方向缩近一个最小价差。
    • 当价格向有利方向移动超过 TrailingStopPoints 时启动跟踪止损。
    • 超过 LiveMinutes 或到达 FridayCloseHour 时可强制平仓。
    • 绝对净头寸达到 MaxDirectionalPositions * TradeVolume 时不再开仓,并可设置入场冷却时间。

参数

名称 默认值 说明
TradeVolume -5 订单手数。正值为固定手数,负值表示按账户资金百分比换算的下单量,使用当前卖价估算。
TakeProfitPoints 40 入场价到止盈价的距离(最小价格步长单位)。
StopLossPoints 340 入场价到止损价的距离(最小价格步长单位)。
TrailingStopPoints 25 跟踪止损距离(最小价格步长)。达到该利润后开始移动止损。
CciPeriod 14 基础周期上计算 CCI 的回溯长度。
CciLimit 75 多头信号的 CCI 上限,同时用负值镜像控制空头信号。负值与原 EA 一致,只对一侧进行放宽。
MaxDirectionalPositions 1 单方向允许的最大净头寸,按计算出的下单量倍数衡量。
ReentryIntervalMinutes 0 连续开仓之间的最小等待分钟数。
TakeProfitReduceMinutes 600 持仓经过多少分钟后,止盈价缩近一个最小价差,设为 0 表示关闭功能。
LiveMinutes 0 持仓最长存续时间(分钟)。为 0 表示不限制。
VolatilityWindow 100 波动周期滚动窗口的长度。负值改为按振幅统计,0 仅使用最新 K 线。
VolatilityThresholdPoints 1 统计成交量所需的最小 K 线实体(方向性模式)或振幅(无方向模式)。符号决定上/下方向的互换。
FridayCloseHour 22 周五强制平仓的小时(0-23),0 表示不启用。
SpreadLimitPoints 5.5 开仓时允许的最大点差(最小价格步长单位)。
CandleType 1 分钟 负责信号判定和风控的基础周期。
Hour1CandleType 1 小时 H1 趋势确认用的周期。
Hour4CandleType 4 小时 H4 趋势确认用的周期。
Minute30CandleType 30 分钟 M30 趋势确认用的周期。
VolatilityCandleType 1 分钟 方向性成交量过滤所使用的周期。

实现细节

  • 策略订阅盘口数据以获取最新买卖价,用于突破确认与点差过滤。
  • 指标绑定完全依赖高阶 API:CCI 通过 BindEx 获取数值,其他周期通过独立订阅管理。
  • 跟踪止损和止盈收缩在代码中手动执行,以保持与原 EA 一致。
  • 负值 TradeVolume 会根据当前卖价与品种的最小手数限制自动调整下单量。

使用方式

  1. 在 Designer 或 Backtester 中将策略连接到投资组合并选择标的。
  2. 配置周期、风险参数以及资金管理设置。
  3. 启动策略。只有已完成的 K 线才会触发信号,开仓使用市价单,平仓由内置风险控制逻辑负责。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Scalpel: CCI-based scalping with EMA filter and ATR stops.
/// </summary>
public class ScalpelStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevRsi;
	private decimal _entryPrice;

	public ScalpelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "RSI period.", "Indicators");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "Trend filter.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevRsi = 0;
		_entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevRsi = 0;
		_entryPrice = 0;

		var rsi = new RelativeStrengthIndex { Length = RsiLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, ema, atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiVal, decimal emaVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_prevRsi == 0 || atrVal <= 0)
		{
			_prevRsi = rsiVal;
			return;
		}

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m || rsiVal > 70)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m || rsiVal < 30)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (rsiVal > 50 && _prevRsi <= 50 && close > emaVal)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (rsiVal < 50 && _prevRsi >= 50 && close < emaVal)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevRsi = rsiVal;
	}
}