MarsiEaStrategy
概览
MarsiEaStrategy 在 StockSharp 高级 API 中复刻了 MetaTrader 上的 MARSIEA 专家顾问。策略以简单移动平均线(SMA)配合相对强弱指标(RSI)判定方向,并且在任意时刻仅持有一笔仓位。止损与止盈均以点(pip)为单位,与原版保持一致;下单手数按账户权益和风险百分比动态计算。
交易逻辑
数据准备
- 在所选 K 线序列上计算可配置周期的 SMA。
- 使用相同的 K 线计算可配置周期的 RSI。
- K 线类型通过
CandleType参数设置,默认使用 1 分钟 K 线。
入场条件
- 只有在两个指标都完成计算且当前没有持仓时才会评估信号。
- 做多: 收盘价位于 SMA 之上,同时 RSI 低于超卖阈值。
- 做空: 收盘价位于 SMA 之下,同时 RSI 高于超买阈值。
- 为保持与原版一致,策略在任何仓位未平仓时不会再次开仓。
离场条件
- 每次开仓后立即登记以点数定义的固定止损和止盈。
- 不设额外离场规则,保护单会负责平仓。
风险控制与仓位管理
RiskPercent决定每笔交易愿意承担的账户权益百分比。- Pip 数值根据
Security.PriceStep、Security.StepPrice以及品种的小数位计算,复刻 MQL 中_Digits的判断方式。 - 手数会按照
Security.VolumeStep四舍五入,并遵守Security.VolumeMin指定的最小交易量。 - 若因缺少品种信息或止损距离为零而无法完成风险计算,策略会退回到
Volume属性(默认 1 手)。
参数说明
| 参数 | 说明 |
|---|---|
CandleType |
指标使用的 K 线序列。 |
MaPeriod |
SMA 的计算周期。 |
RsiPeriod |
RSI 的计算周期。 |
RsiOverbought |
触发做空的 RSI 超买阈值。 |
RsiOversold |
触发做多的 RSI 超卖阈值。 |
RiskPercent |
每笔交易承担的权益百分比。 |
StopLossPips |
以点数表示的止损距离。 |
TakeProfitPips |
以点数表示的止盈距离。 |
转换说明
- 原始 EA 在买卖价上开仓;由于高级 API 不提供逐笔报价,这个移植版本使用 K 线收盘价作为入场参考。
- Pip 计算遵循原版逻辑:当品种保留 5 或 3 位小数时,pip 等于价格步长的 10 倍。
- 调用
StartProtection()以便框架自动为持仓附加止损/止盈订单。 - 策略完全保留“持仓期间不再开新仓”的原始行为。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Moving average plus RSI strategy ported from the MARSIEA MetaTrader expert.
/// Executes a single position at a time with fixed stop-loss and take-profit levels measured in pips.
/// </summary>
public class MarsiEaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiOverbought;
private readonly StrategyParam<decimal> _rsiOversold;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private SimpleMovingAverage _sma;
private RelativeStrengthIndex _rsi;
private decimal? _virtualStopPrice;
private decimal? _virtualTakePrice;
/// <summary>
/// Candle type used to feed indicators.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// RSI period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Overbought threshold for RSI.
/// </summary>
public decimal RsiOverbought
{
get => _rsiOverbought.Value;
set => _rsiOverbought.Value = value;
}
/// <summary>
/// Oversold threshold for RSI.
/// </summary>
public decimal RsiOversold
{
get => _rsiOversold.Value;
set => _rsiOversold.Value = value;
}
/// <summary>
/// Risk percentage used to size the entry volume.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MarsiEaStrategy"/> class.
/// </summary>
public MarsiEaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Series used for indicator calculations", "General");
_maPeriod = Param(nameof(MaPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Simple moving average length", "Indicators")
.SetOptimize(5, 50, 1);
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI lookback length", "Indicators")
.SetOptimize(5, 50, 1);
_rsiOverbought = Param(nameof(RsiOverbought), 55m)
.SetDisplay("RSI Overbought", "Upper RSI threshold", "Signals");
_rsiOversold = Param(nameof(RsiOversold), 45m)
.SetDisplay("RSI Oversold", "Lower RSI threshold", "Signals");
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetGreaterThanZero()
.SetDisplay("Risk Percent", "Equity percentage risked per trade", "Money Management");
_stopLossPips = Param(nameof(StopLossPips), 100m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 300m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_sma = null;
_rsi = null;
_virtualStopPrice = null;
_virtualTakePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_sma = new SimpleMovingAverage { Length = MaPeriod };
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_sma, _rsi, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _sma);
DrawIndicator(area, _rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_sma.IsFormed || !_rsi.IsFormed)
return;
// Check virtual SL/TP
if (Position > 0m)
{
if (_virtualStopPrice.HasValue && candle.LowPrice <= _virtualStopPrice.Value)
{
SellMarket(Position);
_virtualStopPrice = null;
_virtualTakePrice = null;
return;
}
if (_virtualTakePrice.HasValue && candle.HighPrice >= _virtualTakePrice.Value)
{
SellMarket(Position);
_virtualStopPrice = null;
_virtualTakePrice = null;
return;
}
}
else if (Position < 0m)
{
if (_virtualStopPrice.HasValue && candle.HighPrice >= _virtualStopPrice.Value)
{
BuyMarket(Math.Abs(Position));
_virtualStopPrice = null;
_virtualTakePrice = null;
return;
}
if (_virtualTakePrice.HasValue && candle.LowPrice <= _virtualTakePrice.Value)
{
BuyMarket(Math.Abs(Position));
_virtualStopPrice = null;
_virtualTakePrice = null;
return;
}
}
// Only one position can be active at the same time
if (Position != 0m)
return;
var closePrice = candle.ClosePrice;
var volume = CalculateTradeVolume();
if (volume <= 0m)
return;
var pipSize = GetPipSize();
if (pipSize <= 0m)
pipSize = 1m;
if (closePrice > maValue && rsiValue < RsiOversold)
{
BuyMarket(volume);
_virtualStopPrice = closePrice - StopLossPips * pipSize;
_virtualTakePrice = closePrice + TakeProfitPips * pipSize;
}
else if (closePrice < maValue && rsiValue > RsiOverbought)
{
SellMarket(volume);
_virtualStopPrice = closePrice + StopLossPips * pipSize;
_virtualTakePrice = closePrice - TakeProfitPips * pipSize;
}
}
private decimal CalculateTradeVolume()
{
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
var priceStep = Security?.PriceStep ?? 0m;
var stepPrice = 1m;
var pipSize = GetPipSize();
if (RiskPercent <= 0m || portfolioValue <= 0m || priceStep <= 0m || stepPrice <= 0m || pipSize <= 0m)
return NormalizeVolume(Volume > 0m ? Volume : 1m);
var riskAmount = portfolioValue * RiskPercent / 100m;
var perUnitRisk = StopLossPips * pipSize / priceStep * stepPrice;
if (StopLossPips <= 0m || perUnitRisk <= 0m)
return NormalizeVolume(Volume > 0m ? Volume : 1m);
var volume = riskAmount / perUnitRisk;
return NormalizeVolume(volume);
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
volume = 1m;
var volumeStep = Security?.VolumeStep ?? 0m;
if (volumeStep > 0m)
{
var steps = Math.Max(1m, Math.Round(volume / volumeStep, MidpointRounding.AwayFromZero));
volume = steps * volumeStep;
}
var minVolume = Security?.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
return volume;
}
private decimal CalculatePriceSteps(decimal pips)
{
if (pips <= 0m)
return 0m;
var priceStep = Security?.PriceStep ?? 0m;
var pipSize = GetPipSize();
if (priceStep <= 0m || pipSize <= 0m)
return 0m;
var steps = pips * pipSize / priceStep;
return steps > 0m ? steps : 0m;
}
private decimal GetPipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return 0m;
var decimals = Security?.Decimals ?? 0;
return decimals == 3 || decimals == 5 ? priceStep * 10m : priceStep;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class marsi_ea_strategy(Strategy):
"""MA + RSI strategy with virtual SL/TP in pips."""
def __init__(self):
super(marsi_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Series used for indicator calculations", "General")
self._ma_period = self.Param("MaPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("MA Period", "Simple moving average length", "Indicators")
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("RSI Period", "RSI lookback length", "Indicators")
self._rsi_overbought = self.Param("RsiOverbought", 55.0) \
.SetDisplay("RSI Overbought", "Upper RSI threshold", "Signals")
self._rsi_oversold = self.Param("RsiOversold", 45.0) \
.SetDisplay("RSI Oversold", "Lower RSI threshold", "Signals")
self._stop_loss_pips = self.Param("StopLossPips", 100.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 300.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._virtual_stop_price = None
self._virtual_take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@property
def RsiOverbought(self):
return self._rsi_overbought.Value
@property
def RsiOversold(self):
return self._rsi_oversold.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
def OnReseted(self):
super(marsi_ea_strategy, self).OnReseted()
self._virtual_stop_price = None
self._virtual_take_price = None
def OnStarted2(self, time):
super(marsi_ea_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self.MaPeriod
rsi = RelativeStrengthIndex()
rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(sma, rsi, self._process_candle).Start()
def _process_candle(self, candle, ma_value, rsi_value):
if candle.State != CandleStates.Finished:
return
ma_v = float(ma_value)
rsi_v = float(rsi_value)
if self.Position > 0:
if self._virtual_stop_price is not None and float(candle.LowPrice) <= self._virtual_stop_price:
self.SellMarket(self.Position)
self._virtual_stop_price = None
self._virtual_take_price = None
return
if self._virtual_take_price is not None and float(candle.HighPrice) >= self._virtual_take_price:
self.SellMarket(self.Position)
self._virtual_stop_price = None
self._virtual_take_price = None
return
elif self.Position < 0:
if self._virtual_stop_price is not None and float(candle.HighPrice) >= self._virtual_stop_price:
self.BuyMarket(abs(self.Position))
self._virtual_stop_price = None
self._virtual_take_price = None
return
if self._virtual_take_price is not None and float(candle.LowPrice) <= self._virtual_take_price:
self.BuyMarket(abs(self.Position))
self._virtual_stop_price = None
self._virtual_take_price = None
return
if self.Position != 0:
return
close_price = float(candle.ClosePrice)
pip_size = self._get_pip_size()
if pip_size <= 0:
pip_size = 1.0
if close_price > ma_v and rsi_v < float(self.RsiOversold):
self.BuyMarket()
self._virtual_stop_price = close_price - float(self.StopLossPips) * pip_size
self._virtual_take_price = close_price + float(self.TakeProfitPips) * pip_size
elif close_price < ma_v and rsi_v > float(self.RsiOverbought):
self.SellMarket()
self._virtual_stop_price = close_price + float(self.StopLossPips) * pip_size
self._virtual_take_price = close_price - float(self.TakeProfitPips) * pip_size
def _get_pip_size(self):
if self.Security is None or self.Security.PriceStep is None:
return 0.0
price_step = float(self.Security.PriceStep)
if price_step <= 0:
return 0.0
decimals = self.Security.Decimals if self.Security.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return price_step * 10.0
return price_step
def CreateClone(self):
return marsi_ea_strategy()