GitHub で見る
Martin Martingale戦略
概要
この戦略は、現在の価格周辺にヘッジ型マーチンゲールグリッドを構築することで、MQLのオリジナル「Martin」エキスパートアドバイザーの動作を再現します。バスケット全体の累積利益が設定されたターゲットに達するまで、各反転時に取引量を倍増させながら、ロングとショートのポジションを継続的に交互に切り替えます。ローソク足は意思決定ロジックのドライバーとしてのみ使用され、実際の約定はStockSharpの高レベルAPIが公開する成行注文とストップ注文に依存します。
動作原理
- 起動時に、戦略は
EntryOffsetPointsとStepPointsパラメーターを絶対価格距離に変換するためにインストゥルメントのPriceStepを読み取ります。価格ステップが欠如している場合、値1が仮定されます。
- オープンポジションも有効なマーチンゲールサイクルもない場合、戦略は最後のクローズ価格周辺に買いストップと売りストップを配置します。オフセットは
EntryOffsetPoints * PriceStepで、元のMQLコードで使用される10ポイント距離と一致します。
- いずれかのストップ注文が約定すると、反対側の保留中の注文がキャンセルされます。約定がマーチンゲールシーケンスの最初のトレードを定義します。戦略はその価格、量、方向を保存し、内部レベルカウンターを1に設定します。
- 後続の各ローソク足クローズ時に、現在のクローズ価格が最後に約定した注文の価格と比較されます。市場がその注文に対して少なくとも
martingaleLevel * StepPoints * PriceStep動いた場合、前のトレードの2倍の量で反対方向に成行注文が送信されます。最終トレード情報は各実行後に更新されます。
- 未実現利益は
PnL + Position * (closePrice - PositionPrice)として評価されます。この集計利益がProfitTargetパラメーターを超えると、戦略はバスケット内のすべてのポジションをフラット化するためにCloseAll()を送信し、残りのすべての注文をキャンセルして、新しいストップ注文のペアを配置できるようにサイクルをリセットします。
- すべてのポジションが手動でクローズされると、同じリセットが自動的に行われます。内部カウンターがクリアされ、次のローソク足で新しいストップ注文が作成されます。
このワークフローは、元のエキスパートアドバイザーの交互買い/売りロジックを反映しながら、実装をStockSharpの高レベルAPI内に完全に保ちます。
パラメーター
StepPoints – 次の平均化注文の反転閾値を計算するために使用される価格ステップ数。デフォルト10で最適化可能。
EntryOffsetPoints – 初期買い/売りストップ注文のオフセット(価格ステップ)。MQLバージョンと同様にデフォルト10ポイント。
ProfitTarget – マーチンゲールバスケット全体をクローズするために必要な絶対通貨利益。実現・未実現PnLの合計がこの値を超えると、すべてのポジションが清算されます。
CandleType – 戦略ロジックを駆動するためのローソク足サブスクリプション。デフォルトは1分足ですが、市場がサポートする任意のDataTypeを選択できます。
基本取引サイズは戦略のVolumeプロパティから取得されます。各新しい反転は、古典的なマーチンゲール方式でこのベースを2の累乗で乗算します。
実用的な注意事項
- ブローカーの最小ロットサイズに合わせて
Volumeを常に設定してください。倍増スキームにより急速にエクスポージャーが増加するため、リスク制限は外部で適用する必要があります。
- 注文配置はローソク足クローズによって駆動されるため、ローソク足内の急速な価格変動により、ティックベースのMQLバージョンよりもわずかに遅いエントリーが引き起こされる可能性があります。ただし、ストップ注文はエントリー価格を元のロジックと一致させ続けます。
- 戦略はデフォルトチャートエリアに価格ローソク足と自己トレードを描画し、視覚的な追跡を容易にします。
- 自動ストップロスは使用されません。唯一の退出条件は
ProfitTargetであるため、大きな不利なトレンドのリスクをコントロールするためにインストゥルメントと時間軸を慎重に選択する必要があります。
MQLエキスパートとの違い
- StockSharpはネットポジションを使用するため、各反転は単一のトレードで以前のエクスポージャーをクローズし、新しいものをオープンする成行注文で実行されます。バスケットの累積PnLはヘッジ実装と同一のままです。
- ティック毎のロジックは、推奨される高レベルAPI使用法に準拠するために、シグナル評価のためのローソク足クローズに置き換えられました。
- 部分的な約定を複数回処理するのを避けるために注文識別子が追跡され、量の倍増ロジックが一貫したままであることが保証されます。
これらの変更により、取引動作はソース戦略に忠実に保ちながら、StockSharpフレームワークに適応しています。
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>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
private readonly StrategyParam<int> _stepPoints;
private readonly StrategyParam<int> _entryOffsetPoints;
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<int> _maxLevel;
private readonly StrategyParam<DataType> _candleType;
private decimal _stepSize;
private decimal _entryOffset;
private decimal _lastTradePrice;
private decimal _lastTradeVolume;
private int _martingaleLevel;
private Sides? _lastTradeSide;
private bool _isClosing;
private decimal? _initialPrice;
/// <summary>
/// Distance in points that defines when the next reversal is triggered.
/// </summary>
public int StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
/// <summary>
/// Offset in points for the initial breakout entry.
/// </summary>
public int EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Aggregated profit required to close the entire martingale cycle.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum martingale doubling level before resetting.
/// </summary>
public int MaxLevel
{
get => _maxLevel.Value;
set => _maxLevel.Value = value;
}
/// <summary>
/// Candle type used to monitor the price.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
/// </summary>
public MartinMartingaleStrategy()
{
_stepPoints = Param(nameof(StepPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
;
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
;
_profitTarget = Param(nameof(ProfitTarget), 5m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
;
_maxLevel = Param(nameof(MaxLevel), 5)
.SetGreaterThanZero()
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetCycle();
_isClosing = false;
_initialPrice = null;
_stepSize = 0;
_entryOffset = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdateStepSettings();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateStepSettings();
if (_stepSize <= 0m || Volume <= 0m)
return;
var price = candle.ClosePrice;
// If closing, flatten and wait
if (_isClosing)
{
if (Position == 0)
{
_isClosing = false;
ResetCycle();
}
return;
}
// If flat after a cycle, reset
if (Position == 0 && _martingaleLevel > 0)
{
ResetCycle();
}
// Check profit target
if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Max level reached -> close and reset
if (_martingaleLevel >= MaxLevel && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Initial entry: wait for breakout from first candle
if (_martingaleLevel == 0 && Position == 0)
{
if (!_initialPrice.HasValue)
{
_initialPrice = price;
return;
}
if (_entryOffset <= 0m)
return;
if (price >= _initialPrice.Value + _entryOffset)
{
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Buy;
_martingaleLevel = 1;
_initialPrice = null;
}
else if (price <= _initialPrice.Value - _entryOffset)
{
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Sell;
_martingaleLevel = 1;
_initialPrice = null;
}
return;
}
if (_lastTradeSide is null || _martingaleLevel == 0)
return;
var threshold = _stepSize;
if (_lastTradeSide == Sides.Buy)
{
if (price <= _lastTradePrice - threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Sell;
_martingaleLevel++;
}
}
else
{
if (price >= _lastTradePrice + threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Buy;
_martingaleLevel++;
}
}
}
private void UpdateStepSettings()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 1m;
}
_stepSize = StepPoints * priceStep;
_entryOffset = EntryOffsetPoints * priceStep;
}
private void ResetCycle()
{
_martingaleLevel = 0;
_lastTradePrice = 0m;
_lastTradeVolume = 0m;
_lastTradeSide = null;
}
}
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.Strategies import Strategy
class martin_martingale_strategy(Strategy):
"""Martingale grid that alternates long/short entries while doubling volume."""
def __init__(self):
super(martin_martingale_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
self._entry_offset_points = self.Param("EntryOffsetPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
self._profit_target = self.Param("ProfitTarget", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
self._max_level = self.Param("MaxLevel", 5) \
.SetGreaterThanZero() \
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles for price monitoring", "Data")
self._step_size = 0.0
self._entry_offset = 0.0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._martingale_level = 0
self._last_trade_side = 0 # 0=none, 1=buy, -1=sell
self._is_closing = False
self._initial_price = None
@property
def StepPoints(self):
return int(self._step_points.Value)
@property
def EntryOffsetPoints(self):
return int(self._entry_offset_points.Value)
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def MaxLevel(self):
return int(self._max_level.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _update_step_settings(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._step_size = self.StepPoints * step
self._entry_offset = self.EntryOffsetPoints * step
def _reset_cycle(self):
self._martingale_level = 0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._last_trade_side = 0
def OnStarted2(self, time):
super(martin_martingale_strategy, self).OnStarted2(time)
self._update_step_settings()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_step_settings()
if self._step_size <= 0 or float(self.Volume) <= 0:
return
price = float(candle.ClosePrice)
# If closing, flatten and wait
if self._is_closing:
if self.Position == 0:
self._is_closing = False
self._reset_cycle()
return
# If flat after a cycle, reset
if self.Position == 0 and self._martingale_level > 0:
self._reset_cycle()
# Check profit target
if float(self.ProfitTarget) > 0 and float(self.PnL) >= float(self.ProfitTarget) and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Max level reached
if self._martingale_level >= self.MaxLevel and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Initial entry: wait for breakout from first candle
if self._martingale_level == 0 and self.Position == 0:
if self._initial_price is None:
self._initial_price = price
return
if self._entry_offset <= 0:
return
if price >= self._initial_price + self._entry_offset:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = 1
self._martingale_level = 1
self._initial_price = None
elif price <= self._initial_price - self._entry_offset:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = -1
self._martingale_level = 1
self._initial_price = None
return
if self._last_trade_side == 0 or self._martingale_level == 0:
return
threshold = self._step_size
if self._last_trade_side == 1:
if price <= self._last_trade_price - threshold:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = -1
self._martingale_level += 1
else:
if price >= self._last_trade_price + threshold:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = 1
self._martingale_level += 1
def OnReseted(self):
super(martin_martingale_strategy, self).OnReseted()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
self._step_size = 0.0
self._entry_offset = 0.0
def CreateClone(self):
return martin_martingale_strategy()