在 GitHub 上查看

The Enchantress 策略

策略概览

The Enchantress 策略完整移植了同名 MQL4 EA 的自学习流程。原版会把每根收盘蜡烛划分到 10 个区间中的一个,维护最近 7 根蜡烛的模式 序列,并在每个 7 位模式上创建一对虚拟买单与卖单。当之后的行情触及虚拟止盈或止损时,该模式会得到正向或负向的评分。只有当当前 模式属于得分最高的模式集合时才会开仓。本移植版在 StockSharp 框架下重现了这种反馈循环,并将关键参数暴露为可调节的策略属性。

蜡烛分类规则

  1. 仅在蜡烛收盘后处理一次,使用开、高、低、收四个价位。
  2. 先根据实体方向区分看空(数字 0–4)与看多(数字 5–9)。
  3. 通过比例 100 - Low * 100 / High 决定具体数字:
    • 0/5:极窄范围(≤ 0.04)
    • 1/6:较窄范围(0.04 – 0.15)
    • 2/7:中等范围(0.15 – 0.25)
    • 3/8:较宽范围(0.25 – 0.40)
    • 4/9:非常宽的波动(> 0.40)
  4. 最新数字会被追加到保存最近 7 根蜡烛的模式窗口。

上述逻辑与原脚本 ManagePatterns 函数生成的编号完全一致。

虚拟订单引擎

  • 当窗口长度达到 7 时,会为该模式建立一对虚拟多空单。
  • 虚拟入场价等于蜡烛收盘价;虚拟止盈/止损根据 VirtualStopLossVirtualTakeProfit 结合品种的 PriceStep 推算。
  • 后续每根蜡烛都会利用最高价/最低价判断虚拟订单是否被触发:
    • 触发止盈为对应多/空分数加 +1
    • 触发止损为该分数减 3,重现 EA 的惩罚逻辑;
  • 已结算的虚拟订单会被移除,累计分数仍绑定在各自的 7 位模式键上,避免内存无限增长。

信号生成流程

在处理下一根蜡烛前,策略会读取当前 7 位模式(仅包含已经结束的蜡烛)。交易窗口限定在周一至周四,周五与原版一样完全跳过。具体 步骤如下:

  1. 按分数从高到低筛选出多头与空头各 10 个领先模式,只考虑分数 ≥ 1 的模式;
  2. 若当前模式属于多头领先集合,则发送市价买单;若属于空头领先集合,则发送市价卖单。策略会记录本蜡烛的时间戳,确保同一根蜡烛 只会触发一次进场;
  3. 完成决策后,把新收盘的蜡烛追加到模式窗口,并为新的 7 位模式生成虚拟订单。

风险控制与仓位

  • 实际下单使用 StopLossTakeProfit 两个以点数表示的参数。策略会按照 PriceStep 把点值转换为价格差,并在市价成交后调用 SetStopLoss/SetTakeProfit 设置保护订单。
  • 仓位控制提供两种模式:
    • 固定手数:直接使用 LotSize,同时根据交易所的 VolumeStep/MinVolume/MaxVolume 校正;
    • 风险比例:当启用 UseRiskMoneyManagementRiskPercent 大于 0 时,手数按 PortfolioValue / 100000 * RiskPercent 计算,与原脚本的 AccountFreeMargin 公式对应;若无法获取组合估值则退回固定手数。

参数说明

参数 说明 默认值
LotSize 关闭风险管理时使用的固定手数。 0.01
UseRiskMoneyManagement 是否启用动态仓位。 true
RiskPercent 动态仓位模式下使用的权益百分比。 15
StopLoss 实际止损距离(点)。 60
VirtualStopLoss 虚拟评分用的止损距离(点)。 55
TakeProfit 实际止盈距离(点)。 19
VirtualTakeProfit 虚拟评分用的止盈距离(点)。 25
CandleType 策略订阅的蜡烛类型/周期。 5m

使用建议

  • 运行前请确认品种的 PriceStepVolumeStepMinVolumeMaxVolume 信息完整,否则点值与手数会退回默认假设。
  • 若需启用风险百分比模式,必须保证 Portfolio.CurrentValuePortfolio.BeginValue 有效;否则策略会恢复为固定手数。
  • 策略仅在蜡烛收盘时评估虚拟订单,使用最高价/最低价近似 MT4 中基于 Tick 的触发条件。
  • 建议先在历史数据上进行回测以快速积累模式评分,实盘与回测共用同一套学习机制。
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// The Enchantress strategy: pattern-based EMA + RSI scoring.
/// Buys when price is above EMA and RSI crosses above 50, sells when below EMA and RSI crosses below 50.
/// </summary>
public class TheEnchantressStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _rsiPeriod;

	private decimal _prevRsi;
	private bool _hasPrev;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }

	public TheEnchantressStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_emaPeriod = Param(nameof(EmaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA trend filter", "Indicators");
		_rsiPeriod = Param(nameof(RsiPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI momentum period", "Indicators");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevRsi = 0;
		_hasPrev = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevRsi = 0;
		_hasPrev = false;
		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ema, rsi, ProcessCandle).Start();
	}

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

		if (_hasPrev)
		{
			if (candle.ClosePrice > emaValue && _prevRsi < 45 && rsiValue >= 45 && Position <= 0)
				BuyMarket();
			else if (candle.ClosePrice < emaValue && _prevRsi > 55 && rsiValue <= 55 && Position >= 0)
				SellMarket();
		}

		_prevRsi = rsiValue;
		_hasPrev = true;
	}
}