在 GitHub 上查看

趋势剥头皮策略(API/3858)

概览

TrendScalperStrategy 是 MetaTrader 4 专家顾问 Currencyprofits_01_1.mq4 的 StockSharp 移植版本。原始脚本是一套轻量的趋势型剥头皮系统:通过 EMA/SMA 交叉确认趋势,并利用最近几个高点和低点的突破来寻找入场机会。本移植版本保持原有交易逻辑,同时使用 StockSharp 的高级蜡烛订阅与指标管线实现。

交易逻辑

  1. 指标
    • 快速 EMA(默认 6)基于收盘价。
    • 慢速 SMA(默认 12)基于收盘价。
    • 最近 N 根 K 线的最高价与最低价(默认窗口 6)。
  2. 入场条件
    • 做多:价格触及最近的低点带(Lowest Low),且快速 EMA 高于慢速 SMA。策略按照资金管理规则下达市价买单。
    • 做空:价格触及最近的高点带(Highest High),且快速 EMA 低于慢速 SMA。策略发送市价卖单。
    • 持仓期间不会再开新仓,与 MQL 版本的“单笔持仓”行为保持一致。
  3. 出场条件
    • 多单平仓:当 K 线高点突破 Highest High 时,立即市价卖出平仓。
    • 空单平仓:当 K 线低点跌破 Lowest Low 时,市价买入平仓。
    • 若 StopLossPoints > 0,StartProtection 会在开仓时附加平台托管的止损单。

资金管理

与原始 EA 相同,提供三个仓位管理模式:

模式 说明 移植版本的行为
0 固定手数(LotsIfNoMM)。 直接返回 FixedVolume。
<0 根据账户余额和风险因子计算的分数手。 计算 ceil(balance * risk / 10000) / 10,最大不超过 100 手。
>0 根据余额与风险因子的整数手。 采用同一公式,但结果向上取整为整数,最少 1 手、最多 100 手。

账户余额优先使用 Portfolio.CurrentValue,若不可用则退回到 BeginValue。若依旧无法获取,则使用固定手数以保证回测可运行。

风险控制

  • 止损:StopLossPoints 以价格点(pip)表示,在 OnStarted 中乘以 Security.PriceStep 后传入 StartProtection,由框架自动管理保护性订单。
  • 单笔持仓:只有在 Position == 0 时才允许开新仓,完全复刻原脚本的防重叠仓位逻辑。

参数

名称 默认值 说明
CandleType 15 分钟 计算指标与产生信号所用的蜡烛序列。
FastLength 6 快速 EMA 周期。
SlowLength 12 慢速 SMA 周期。
BreakoutWindow 6 统计最高/最低价时参考的蜡烛数量。
FixedVolume 0.1 手 在关闭资金管理或缺少余额信息时使用的手数。
MoneyManagementMode 0 选择固定、分数或整数手的模式。
MoneyManagementRisk 40 余额驱动仓位大小的风险系数。
StopLossPoints 50 止损距离(点数),会在启动时换算成绝对价格。

实现细节

  • 通过 SubscribeCandles().Bind(...) 串联指标,不需要自行维护历史价格缓存。
  • 代码中的注释全部使用英文,以符合仓库规范。
  • 未改动任何测试文件,本次提交仅包含策略移植与文档。

使用建议

  • 根据原策略的定位选择合适的短周期(剥头皮)K 线。
  • 确保标的证券的 PriceStep 已正确设置,以便止损距离能从点数准确换算为价格。
  • 调整 MoneyManagementRisk 时需谨慎,值越大意味着通过 ceil(balance * risk / 10000) 公式计算出的仓位越大。

精确定义: 通道最高价与最低价取自信号K线之前的 BreakoutWindow 根已完成 K线,不包含当前K线。买入要求 EMA(FastLength) > SMA(SlowLength) 且 currentLow <= previousWindowLow;卖出要求反向趋势且 currentHigh >= previousWindowHigh。持有多头时 currentHigh > previousWindowHigh 平仓,空头时 currentLow < previousWindowLow 平仓。所有指标与通道需完成预热。当 StopLossPoints > 0 时使用 StartProtection 设置保护。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

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

/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _breakoutWindow;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _moneyManagementMode;
	private readonly StrategyParam<decimal> _moneyManagementRisk;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _fastSeed = new();
	private readonly Queue<decimal> _slowCloses = new();
	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();
	private decimal? _fastEma;
	private Order _pendingOrder;

	public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
	public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
	public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
	public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
	public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
	public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public TrendScalperStrategy()
	{
		_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
		_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
		_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
		_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
		_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
		_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
		_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_fastSeed.Clear();
		_slowCloses.Clear();
		_highs.Clear();
		_lows.Clear();
		_fastEma = null;
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");

		if (StopLossPoints > 0m)
		{
			StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
				useMarketOrders: true, isLocalStop: true);
			foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
			{
				var quotes = new Subscription(DataType.Level1, Security);
				quotes.MarketData.BuildField = field;
				SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
			}
		}
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ObserveQuote(Level1ChangeMessage message)
	{
		// A Level1 stream is needed for native stop processing between candle closes.
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var hasBand = _highs.Count == BreakoutWindow;
		var bandHigh = hasBand ? _highs.Max() : 0m;
		var bandLow = hasBand ? _lows.Min() : 0m;
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);
		while (_highs.Count > BreakoutWindow) _highs.Dequeue();
		while (_lows.Count > BreakoutWindow) _lows.Dequeue();

		var price = candle.ClosePrice;
		if (_fastEma is null)
		{
			_fastSeed.Enqueue(price);
			if (_fastSeed.Count == FastLength)
				_fastEma = _fastSeed.Average();
		}
		else
			_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);

		_slowCloses.Enqueue(price);
		while (_slowCloses.Count > SlowLength)
			_slowCloses.Dequeue();
		if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
			_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
			return;

		if (Position > 0m && candle.HighPrice > bandHigh)
		{
			Submit(Sides.Sell, Position, "Trend scalper band exit");
			return;
		}
		if (Position < 0m && candle.LowPrice < bandLow)
		{
			Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
			return;
		}
		if (Position != 0m)
			return;

		var slow = _slowCloses.Average();
		if (_fastEma > slow && candle.LowPrice <= bandLow)
			Enter(Sides.Buy);
		else if (_fastEma < slow && candle.HighPrice >= bandHigh)
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = CalculateOrderVolume();
		if (volume > 0m)
			Submit(side, volume, "Trend scalper entry");
	}

	private decimal CalculateOrderVolume()
	{
		var volume = FixedVolume;
		if (MoneyManagementMode != 0)
		{
			var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (balance > 0m)
			{
				var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
				volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
				volume = Math.Min(volume, 100m);
			}
		}
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		return volume;
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		RegisterOrder(_pendingOrder);
	}
}