在 GitHub 上查看
The Enchantress 策略
策略概览
The Enchantress 策略完整移植了同名 MQL4 EA 的自学习流程。原版会把每根收盘蜡烛划分到 10 个区间中的一个,维护最近 7 根蜡烛的模式
序列,并在每个 7 位模式上创建一对虚拟买单与卖单。当之后的行情触及虚拟止盈或止损时,该模式会得到正向或负向的评分。只有当当前
模式属于得分最高的模式集合时才会开仓。本移植版在 StockSharp 框架下重现了这种反馈循环,并将关键参数暴露为可调节的策略属性。
蜡烛分类规则
- 仅在蜡烛收盘后处理一次,使用开、高、低、收四个价位。
- 先根据实体方向区分看空(数字
0–4)与看多(数字 5–9)。
- 通过比例
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)
- 最新数字会被追加到保存最近 7 根蜡烛的模式窗口。
上述逻辑与原脚本 ManagePatterns 函数生成的编号完全一致。
虚拟订单引擎
- 当窗口长度达到 7 时,会为该模式建立一对虚拟多空单。
- 虚拟入场价等于蜡烛收盘价;虚拟止盈/止损根据
VirtualStopLoss 与 VirtualTakeProfit 结合品种的 PriceStep 推算。
- 后续每根蜡烛都会利用最高价/最低价判断虚拟订单是否被触发:
- 触发止盈为对应多/空分数加
+1;
- 触发止损为该分数减
3,重现 EA 的惩罚逻辑;
- 已结算的虚拟订单会被移除,累计分数仍绑定在各自的 7 位模式键上,避免内存无限增长。
信号生成流程
在处理下一根蜡烛前,策略会读取当前 7 位模式(仅包含已经结束的蜡烛)。交易窗口限定在周一至周四,周五与原版一样完全跳过。具体
步骤如下:
- 按分数从高到低筛选出多头与空头各 10 个领先模式,只考虑分数 ≥ 1 的模式;
- 若当前模式属于多头领先集合,则发送市价买单;若属于空头领先集合,则发送市价卖单。策略会记录本蜡烛的时间戳,确保同一根蜡烛
只会触发一次进场;
- 完成决策后,把新收盘的蜡烛追加到模式窗口,并为新的 7 位模式生成虚拟订单。
风险控制与仓位
- 实际下单使用
StopLoss、TakeProfit 两个以点数表示的参数。策略会按照 PriceStep 把点值转换为价格差,并在市价成交后调用
SetStopLoss/SetTakeProfit 设置保护订单。
- 仓位控制提供两种模式:
- 固定手数:直接使用
LotSize,同时根据交易所的 VolumeStep/MinVolume/MaxVolume 校正;
- 风险比例:当启用
UseRiskMoneyManagement 且 RiskPercent 大于 0 时,手数按
PortfolioValue / 100000 * RiskPercent 计算,与原脚本的 AccountFreeMargin 公式对应;若无法获取组合估值则退回固定手数。
参数说明
| 参数 |
说明 |
默认值 |
LotSize |
关闭风险管理时使用的固定手数。 |
0.01 |
UseRiskMoneyManagement |
是否启用动态仓位。 |
true |
RiskPercent |
动态仓位模式下使用的权益百分比。 |
15 |
StopLoss |
实际止损距离(点)。 |
60 |
VirtualStopLoss |
虚拟评分用的止损距离(点)。 |
55 |
TakeProfit |
实际止盈距离(点)。 |
19 |
VirtualTakeProfit |
虚拟评分用的止盈距离(点)。 |
25 |
CandleType |
策略订阅的蜡烛类型/周期。 |
5m |
使用建议
- 运行前请确认品种的
PriceStep、VolumeStep、MinVolume、MaxVolume 信息完整,否则点值与手数会退回默认假设。
- 若需启用风险百分比模式,必须保证
Portfolio.CurrentValue 或 Portfolio.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;
}
}
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 ExponentialMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class the_enchantress_strategy(Strategy):
def __init__(self):
super(the_enchantress_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._ema_period = self.Param("EmaPeriod", 50)
self._rsi_period = self.Param("RsiPeriod", 21)
self._prev_rsi = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsi_period.Value = value
def OnReseted(self):
super(the_enchantress_strategy, self).OnReseted()
self._prev_rsi = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(the_enchantress_strategy, self).OnStarted2(time)
self._prev_rsi = 0.0
self._has_prev = False
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
rsi = RelativeStrengthIndex()
rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, rsi, self._process_candle).Start()
def _process_candle(self, candle, ema_value, rsi_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
rsi_val = float(rsi_value)
if self._has_prev:
if close > ema_val and self._prev_rsi < 45 and rsi_val >= 45 and self.Position <= 0:
self.BuyMarket()
elif close < ema_val and self._prev_rsi > 55 and rsi_val <= 55 and self.Position >= 0:
self.SellMarket()
self._prev_rsi = rsi_val
self._has_prev = True
def CreateClone(self):
return the_enchantress_strategy()