MarsiEa頭脳系
概要
MarsiEaStrategy は、StockSharp の高レベル API 内で元の MetaTrader MARSIEA 専門アドバイザーのロジックを複製します。この戦略は、単純な移動平均と相対強度インデックス (RSI) フィルターを組み合わせたもので、一度に 1 つのポジションのみを保持します。プロテクティブなストップロス注文とテイクプロフィット注文は、ソース実装とまったく同じようにピップ単位で測定され、取引量はポートフォリオの資本から動的に調整されます。
取引ロジック
データの準備
- 構成可能な長さの単純移動平均 (SMA) は、選択したローソク足シリーズで実行されます。
- 期間を設定できる RSI は同じローソク足を使用します。
- ローソク足シリーズは、
CandleTypeパラメータを通じて設定可能で、デフォルトは 1 分間のローソク足です。
エントリールール
- この戦略では、両方のインジケーターが形成され、オープン ポジションが存在しないことが必要です。
- 長い設定: 終値は SMA を超えており、RSI は売られすぎのしきい値を下回っています。
- 簡単な設定: 終値は SMA を下回っており、RSI は買われすぎのしきい値を上回っています。
- MetaTrader のエキスパートの行動を反映し、いつでもオープンできるポジションは 1 つだけです。
退出ルール
- 取引を開始した直後、戦略は固定のストップロスとテイクプロフィットの距離を登録します。両方ともピップで定義されます。
- 追加の終了条件はありません。保護命令はポジションの閉鎖をハンドルします。
リスクとポジションのサイジング
RiskPercentは、取引ごとにリスクがかかる現在のポートフォリオ値の割合を制御します。- pip 値は、
Security.PriceStep、Security.StepPrice、および桁数から計算され、MQL からの_Digitsチェックをエミュレートします。 - 音量は許容される最も近い
Security.VolumeStepに四捨五入され、利用可能な場合はSecurity.VolumeMinが尊重されます。 - リスクベースのサイジングを計算できない場合(商品メタデータが欠落しているかゼロストップ)、戦略は
Volumeプロパティに戻ります(デフォルトは 1 契約/ロット)。
パラメーター
| パラメータ | 説明 |
|---|---|
CandleType |
インジケーターの計算に使用されるローソク足シリーズ。 |
MaPeriod |
SMA インジケーターの長さ。 |
RsiPeriod |
RSI のルックバックの長さ。 |
RsiOverbought |
RSI のしきい値はショートの買われ過ぎ市場を定義します。 |
RsiOversold |
RSI ロングの売られ過ぎ市場を定義するしきい値。 |
RiskPercent |
取引ごとにリスクが生じる株式の割合。 |
StopLossPips |
ピップスで表されるストップロス距離。 |
TakeProfitPips |
ピップスで表されるテイクプロフィット距離。 |
変換時の注意点
- MetaTrader 実装は買値/売値で取引されました。高レベルの API ではバー内ティックが利用できないため、このポートはエントリー参照としてローソク足の終値を使用します。
- ピップサイズは、MQL バージョンと同じルールに従います。つまり、5 桁または 3 桁の記号で価格ステップを 10 倍します。
StartProtection()は 1 回呼び出され、ストップロス注文と利食い注文がエンジンによってオープンポジションに自動的にリンクされます。- この戦略では、ポジションがアクティブである間は新しいエントリをスキップするという元の動作が維持されます。
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()