在 GitHub 上查看
Testinator 策略
概述
本策略是 MetaTrader 智能交易程序 Testinator v1.30a 的 C# 版本。策略只做多,并把所有多单当作一个篮子来管理。只有当一组可配置的技术过滤器全部满足、且价格至少向前推进一定点数时,才会继续加仓。平仓逻辑使用另一组过滤器掩码,结构与开仓完全对称。原版 EA 使用日线 ATR 做风险控制,因此该移植版本除了主级别 K 线外还会订阅日线数据。
交易逻辑
入场过滤掩码(参数 BuySequence)
掩码使用低 9 个二进制位。每个被置 1 的位都必须在上一根完成的 K 线上满足对应条件。
| 位 |
条件 |
| 1 |
EMA(12) 高于 SMA(14)。 |
| 2 |
EMA(50) 低于最近三根 K 线的最低价。 |
| 4 |
前一根 K 线的最低价低于入场布林带下轨(20,2)。 |
| 8 |
ADX(14) 高于 -DI,且 +DI 强于 -DI。 |
| 16 |
随机指标 (16, 4, 8) 的 %K 高于 %D,且 %D 大于 80。 |
| 32 |
威廉指标 %R(14) 大于 -20。 |
| 64 |
MACD(12, 26, 9) 主线高于信号线。 |
| 128 |
一目均衡云的领先线 A 高于领先线 B,转折线高于基准线,并且上一根 K 线的最低价高于领先线 A。 |
| 256 |
RSI(周期 RsiEntryPeriod)高于 RsiEntryLevel 且相比前一根 K 线在上升。 |
出场过滤掩码(参数 CloseBuySequence)
| 位 |
条件 |
| 1 |
SMA(14) 高于 EMA(12)。 |
| 2 |
EMA(50) 高于最近三根 K 线的最高价。 |
| 4 |
前一根 K 线的最高价高于出场布林带上轨(BollingerCloseLength, BollingerCloseDeviation)。 |
| 8 |
-DI 高于 +DI。 |
| 16 |
随机指标的 %D 低于 80。 |
| 32 |
威廉指标 %R(14) 低于 -80。 |
| 64 |
MACD 主线低于信号线。 |
| 128 |
一目均衡云的领先线 B 高于领先线 A。 |
| 256 |
RSI(周期 RsiClosePeriod)低于 RsiCloseLevel。 |
只有当所有启用的入场位都返回真、当前买单数量少于 MaxBuys 并且最新成交价距离上一次加仓至少 StepPips 个点时,策略才会继续加仓。一旦出场掩码满足或保护性价位被触发,就会整体平仓。
交易时间与风险控制
- 策略只在
TradeStartHour 到 TradeStartHour + TradeDurationHours - 1(东欧时间)的时段内交易。若超出交易窗口且篮子处于盈利状态,会立即平掉所有多单。
- 止盈和止损距离以点为单位。设置为
-1 表示关闭;设置为 0 时使用 ATR 乘数(StopRatio、TakeRatio)。
- 跟踪止损同样支持 ATR 逻辑,通过
StartTrailPips、TrailStepPips、StartTrailRatio、TrailStepRatio 来配置。
- 策略在日线数据上计算 ATR(15),以保持与原始 EA 一致的行为。
参数
TradeVolume – 每次市价买单的交易量。
BuySequence / CloseBuySequence – 控制各个指标过滤条件的位掩码。
MaxBuys – 同时持有的最大买单数量。
StepPips – 再次加仓所需的最小价格推进(点)。
TradeStartHour, TradeDurationHours – 日内交易窗口。
TakeProfitPips, StopLossPips – 固定止盈止损(负值关闭,0 表示使用 ATR 比例)。
StartTrailPips, TrailStepPips – 跟踪止损的起始距离和步长(负值关闭,0 表示使用 ATR 比例)。
TakeRatio, StopRatio, StartTrailRatio, TrailStepRatio – 当固定值为零时所使用的 ATR 乘数。
RsiEntryLevel, RsiEntryPeriod – 入场 RSI 的阈值与周期。
RsiCloseLevel, RsiClosePeriod – 出场 RSI 的阈值与周期。
BollingerCloseLength, BollingerCloseDeviation – 出场布林带参数。
CandleType – 主时间框架(策略会自动订阅日线以计算 ATR)。
说明
- 移植版本保持了原 EA 的篮子管理模型:只做多,并仅使用市价单。
- 策略显式保存各指标的历史值,以模拟 MetaTrader 中对
bar[1] 的引用。
- 原 EA 中未使用的输入(例如
TakeAsBasket, StopAsBasket 等)在移植时被忽略,因为它们不会影响逻辑。
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Testinator strategy: RSI-based entry with EMA trend filter.
/// Buys when RSI rises above threshold and price is above EMA.
/// Sells when RSI drops below threshold and price is below EMA.
/// </summary>
public class TestinatorStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<decimal> _rsiBuyLevel;
private readonly StrategyParam<decimal> _rsiSellLevel;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public decimal RsiBuyLevel { get => _rsiBuyLevel.Value; set => _rsiBuyLevel.Value = value; }
public decimal RsiSellLevel { get => _rsiSellLevel.Value; set => _rsiSellLevel.Value = value; }
public TestinatorStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI period", "Indicators");
_emaPeriod = Param(nameof(EmaPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA trend filter period", "Indicators");
_rsiBuyLevel = Param(nameof(RsiBuyLevel), 55m)
.SetDisplay("RSI Buy Level", "RSI threshold for buy signal", "Signals");
_rsiSellLevel = Param(nameof(RsiSellLevel), 45m)
.SetDisplay("RSI Sell Level", "RSI threshold for sell signal", "Signals");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
decimal? prevClose = null;
decimal? prevEma = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ema, (candle, rsiVal, emaVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
if (prevClose.HasValue && prevEma.HasValue)
{
var crossUp = prevClose.Value <= prevEma.Value && close > emaVal;
var crossDown = prevClose.Value >= prevEma.Value && close < emaVal;
if (crossUp && rsiVal > RsiBuyLevel && Position <= 0)
BuyMarket();
else if (crossDown && rsiVal < RsiSellLevel && Position >= 0)
SellMarket();
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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 RelativeStrengthIndex, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class testinator_strategy(Strategy):
def __init__(self):
super(testinator_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetDisplay("RSI Period", "RSI period", "Indicators")
self._ema_period = self.Param("EmaPeriod", 50) \
.SetDisplay("EMA Period", "EMA trend filter period", "Indicators")
self._rsi_buy_level = self.Param("RsiBuyLevel", 55.0) \
.SetDisplay("RSI Buy Level", "RSI threshold for buy signal", "Signals")
self._rsi_sell_level = self.Param("RsiSellLevel", 45.0) \
.SetDisplay("RSI Sell Level", "RSI threshold for sell signal", "Signals")
self._rsi = None
self._ema = None
self._prev_close = None
self._prev_ema = None
@property
def rsi_period(self):
return self._rsi_period.Value
@property
def ema_period(self):
return self._ema_period.Value
@property
def rsi_buy_level(self):
return self._rsi_buy_level.Value
@property
def rsi_sell_level(self):
return self._rsi_sell_level.Value
def OnReseted(self):
super(testinator_strategy, self).OnReseted()
self._rsi = None
self._ema = None
self._prev_close = None
self._prev_ema = None
def OnStarted2(self, time):
super(testinator_strategy, self).OnStarted2(time)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.rsi_period
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
subscription.Bind(self._rsi, self._ema, self._process_candle)
subscription.Start()
def _process_candle(self, candle, rsi_value, ema_value):
if candle.State != CandleStates.Finished:
return
if not self._rsi.IsFormed or not self._ema.IsFormed:
return
close = float(candle.ClosePrice)
rsi_val = float(rsi_value)
ema_val = float(ema_value)
if self._prev_close is not None and self._prev_ema is not None:
cross_up = self._prev_close <= self._prev_ema and close > ema_val
cross_down = self._prev_close >= self._prev_ema and close < ema_val
if cross_up and rsi_val > self.rsi_buy_level and self.Position <= 0:
self.BuyMarket()
elif cross_down and rsi_val < self.rsi_sell_level and self.Position >= 0:
self.SellMarket()
self._prev_close = close
self._prev_ema = ema_val
def CreateClone(self):
return testinator_strategy()