Precipice Martin戦略 (C#)
概要
Precipice Martin戦略は、処理された各ローソク足のクローズ時に1つの成行注文を開く機械的なグリッドアプローチです。元のMetaTrader 5エキスパートアドバイザーは、各新しいバーに対称的な買いと売りのポジションを作成し、pipsで表現された静的なストップロスとテイクプロフィットのオフセットを使用して出口を管理しました。損失取引は次の注文サイズをマルチンゲール乗数で増加させ、利益取引はポジションサイズを最小ロットにリセットしました。
このC#ポートはStockSharpの高レベルAPIを使用して同じ高レベルロジックに従います。各完了ローソク足に対して戦略は:
- 既存のロングとショートのポジションを更新し、ローソク足の範囲が設定されたストップロスまたはテイクプロフィットレベルを突き破った場合はそれらをクローズします。
- フラット時、ソースロボットのデュアルエントリー動作をエミュレートしながら、StockSharpのネットポジション会計と互換性を保ちつつ(両方向が有効な場合)、連続した機会にロングとショートの市場ポジションを交互に開きます。
- オプションのマルチンゲールサイジングを適用し、連続した損失取引が設定された乗数によってボリュームを増加させます。
- ユーザー定義のpip距離からストップロスとテイクプロフィットのターゲットを計算し、インストゥルメントのティックサイズに基づいて絶対価格オフセットに変換します。
変換ノート
- 元のEAは、両方のトグルが有効な場合、各新しいバーにロングとショートのポジションを開きました。StockSharpはデフォルトでネットポジションを使用するため、C#バージョンはネットポジションを即座にフラット化するのを避けるために、連続した機会に方向を交互に切り替えます。これにより、時間の経過とともに市場の両側が取引されることが保証されます。
- ストップロスとテイクプロフィットの管理は、ローソク足の高値/安値が対応するレベルをトリガーしたかどうかを確認することで内部的に実行されます。レベルに達すると、戦略は成行注文を使用してポジションをクローズし、マルチンゲールロジックのために実現利益または損失を記録します。
- ロット検証は、計算されたボリュームをexchangeの
VolumeStepに丸め、最小と最大の境界を強制し、丸められた値がゼロになった場合に注文をキャンセルすることでMQL5のLotCheckルーチンを複製します。 - マルチンゲールルーチンは
CalculateLotを反映します:利益のない出口はすべて次の注文サイズをMartingaleCoefficientで乗算し、利益のある出口は乗数を1にリセットします。
パラメーター
| パラメーター | 説明 |
|---|---|
| Use Buy | ロングポジションの開設を有効にする。 |
| Buy SL/TP (pips) | ロング取引のストップロスとテイクプロフィット両方に使用される距離(pips)。値が0の場合、その側の出口が無効になる。 |
| Use Sell | ショートポジションの開設を有効にする。 |
| Sell SL/TP (pips) | ショート取引のストップロスとテイクプロフィット両方に使用される距離(pips)。 |
| Use Martingale | マルチンゲールポジションサイジングを切り替える。無効化されると各注文は最小ロットサイズを使用する。 |
| Martingale Coefficient | 利益のない各取引後に最小ロットに適用される乗数。 |
| Candle Type | 戦略が処理するローソク足の時間軸。デフォルトでは戦略は1分バーで動作するが、利用可能な任意の時間軸を選択できる。 |
取引ロジック
- Pip サイズ計算 – 戦略はインストゥルメントのティックサイズからpip値を導出します。分数pip(5桁のFXシンボル)で見積もられるインストゥルメントの場合、pipはMT5実装に合わせて10ティックとみなされます。
- エントリー選択 –
Use BuyとUse Sellの両方が有効な場合、戦略はフラット時にロングとショートのエントリーを交互に行います。1つの方向のみが有効な場合、すべての取引はその方向に従います。エントリーはローソク足が完了し、戦略がオンラインの直後に発動されます。 - ストップ/テイクレベル – 取引が開かれると、ストップロスとテイクプロフィットは選択されたpip距離を使用してエントリーを基準とした絶対価格として保存されます。値がゼロの場合、その方向の両方のレベルが無効になります。
- 出口処理 – 各完了ローソク足で高値/安値の値が確認されます。安値がロングストップを下回るか、高値がロングターゲットを超えると、ロングポジションがクローズされます。ショートの場合、ロジックは反転されます。出口はそのポジションの最後に記録されたボリュームを使用した成行注文で実行されます。
- マルチンゲールサイジング – 次の注文ボリュームは、現在のマルチンゲール乗数で乗算されたインストゥルメントの最小ロットと等しい。損失取引(損益分岐点の結果を含む)は乗数を
MartingaleCoefficientで乗算し、利益取引はそれを1にリセットします。注文が送信される前にexchangeステップへのボリューム丸めが適用されます。 - 安全チェック – 丸められたボリュームがexchangeの最小ロット未満の場合、注文はスキップされ、元のEAが
CheckVolumeで処理した「資金不足」エラーを防ぎます。
使用ガイドライン
- MT5で使用するチャート期間に合わせてCandle Typeで希望の時間軸を設定する。
- 希望するストップロスとテイクプロフィットの動作に合わせてpip距離を調整する。オフセットは絶対価格であるため、通貨での実際のストップはシンボルに依存することに注意する。
- リスク許容度に応じてマルチンゲールサイジングを有効または無効にする。連続した損失後にボリュームが指数関数的に成長するため、保守的な乗数を適用する。
- リアルタイムローソク足を提供するインストゥルメントに戦略を展開する。戦略は操作に完了したバーを必要とし、不完全なローソク足では取引しない。
- マルチンゲールがアクティブな場合、マージンの使用を監視する。StockSharpバージョンは両側が有効な場合に意図的に方向を交互に切り替えるため、任意の時点で1つのネットポジションのみが開いている。
MT5実装との違い
- ネットポジション – 交互ロジックは元のアルゴリズムの同時ヘッジエントリーを置き換えます。真のヘッジ口座が必要な場合は、2つの戦略インスタンスを実行できます(1つは
Use Buy、もう1つはUse Sell)。 - 注文配置 – 保護注文はexchangeブックに配置されません。代わりに、戦略がストップまたはテイクレベルが超えられたことを検出すると成行注文で出口が実行されます。
- 履歴スキャン – MT5スクリプトはすべての取引履歴を各ティックでスキャンすることでマルチンゲール係数を再計算しました。C#バージョンはオーバーヘッドを減らしながら動作を維持するために乗数を増分的に維持します。
リスク免責事項
マルチンゲールベースの戦略は、損失連続中に非常に大きなポジションを生成する可能性があり、口座リスク制限を超える可能性があります。ライブ展開前に常にシミュレートされたデータで戦略をテストし、選択した乗数とpip距離が取引インストゥルメントのボラティリティに適していることを確認してください。
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>
/// Grid style strategy that opens a position on every new bar with optional martingale sizing.
/// </summary>
public class PrecipiceMartinStrategy : Strategy
{
private readonly StrategyParam<bool> _useBuy;
private readonly StrategyParam<int> _buyStepPips;
private readonly StrategyParam<bool> _useSell;
private readonly StrategyParam<int> _sellStepPips;
private readonly StrategyParam<bool> _useMartingale;
private readonly StrategyParam<decimal> _martingaleCoefficient;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipSize;
private decimal _martingaleMultiplier;
private decimal? _longEntryPrice;
private decimal? _longStopPrice;
private decimal? _longTakePrice;
private decimal? _shortEntryPrice;
private decimal? _shortStopPrice;
private decimal? _shortTakePrice;
private decimal _lastLongVolume;
private decimal _lastShortVolume;
private bool _preferLongEntry;
public bool UseBuy
{
get => _useBuy.Value;
set => _useBuy.Value = value;
}
public int BuyStepPips
{
get => _buyStepPips.Value;
set => _buyStepPips.Value = value;
}
public bool UseSell
{
get => _useSell.Value;
set => _useSell.Value = value;
}
public int SellStepPips
{
get => _sellStepPips.Value;
set => _sellStepPips.Value = value;
}
public bool UseMartingale
{
get => _useMartingale.Value;
set => _useMartingale.Value = value;
}
public decimal MartingaleCoefficient
{
get => _martingaleCoefficient.Value;
set => _martingaleCoefficient.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PrecipiceMartinStrategy()
{
_useBuy = Param(nameof(UseBuy), true)
.SetDisplay("Use Buy", "Enable opening long positions", "Trading");
_buyStepPips = Param(nameof(BuyStepPips), 89)
.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading");
_useSell = Param(nameof(UseSell), true)
.SetDisplay("Use Sell", "Enable opening short positions", "Trading");
_sellStepPips = Param(nameof(SellStepPips), 89)
.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading");
_useMartingale = Param(nameof(UseMartingale), true)
.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing");
_martingaleCoefficient = Param(nameof(MartingaleCoefficient), 1.6m)
.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_martingaleMultiplier = 1m;
_longEntryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_lastLongVolume = 0m;
_lastShortVolume = 0m;
_preferLongEntry = true;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Calculate the pip size based on the instrument tick size.
_pipSize = (Security?.PriceStep ?? 1m) * 10m;
if (_pipSize <= 0m)
_pipSize = Security?.PriceStep ?? 1m;
if (_pipSize <= 0m)
_pipSize = 1m;
_martingaleMultiplier = 1m;
// Subscribe to candle data and process every completed bar.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Ignore unfinished candles because the original strategy trades on bar close.
if (candle.State != CandleStates.Finished)
return;
// Manage exits before looking for new entries.
var closedLong = TryCloseLong(candle);
var closedShort = TryCloseShort(candle);
// Do not open new trades while any position is still active.
if (Position != 0)
return;
// Avoid immediate re-entry for a direction that has just closed on this bar.
if (closedLong)
return;
if (closedShort)
return;
if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
return;
if (UseBuy && UseSell)
{
if (_preferLongEntry)
{
if (TryEnterLong(candle))
{
_preferLongEntry = false;
return;
}
if (TryEnterShort(candle))
{
_preferLongEntry = false;
}
}
else
{
if (TryEnterShort(candle))
{
_preferLongEntry = true;
return;
}
if (TryEnterLong(candle))
{
_preferLongEntry = true;
}
}
}
else
{
if (UseBuy)
{
TryEnterLong(candle);
}
if (UseSell)
{
TryEnterShort(candle);
}
}
}
private bool TryEnterLong(ICandleMessage candle)
{
// Prevent duplicate long entries.
if (_longEntryPrice.HasValue)
return false;
// Ensure no net position exists before opening a new long.
if (Position != 0)
return false;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = candle.ClosePrice;
Volume = volume;
BuyMarket();
_longEntryPrice = entryPrice;
_lastLongVolume = volume;
if (BuyStepPips > 0)
{
var offset = BuyStepPips * _pipSize;
_longStopPrice = entryPrice - offset;
_longTakePrice = entryPrice + offset;
}
else
{
_longStopPrice = null;
_longTakePrice = null;
}
return true;
}
private bool TryEnterShort(ICandleMessage candle)
{
// Prevent duplicate short entries.
if (_shortEntryPrice.HasValue)
return false;
// Ensure no net position exists before opening a new short.
if (Position != 0)
return false;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = candle.ClosePrice;
Volume = volume;
SellMarket();
_shortEntryPrice = entryPrice;
_lastShortVolume = volume;
if (SellStepPips > 0)
{
var offset = SellStepPips * _pipSize;
_shortStopPrice = entryPrice + offset;
_shortTakePrice = entryPrice - offset;
}
else
{
_shortStopPrice = null;
_shortTakePrice = null;
}
return true;
}
private bool TryCloseLong(ICandleMessage candle)
{
if (!_longEntryPrice.HasValue)
return false;
var volume = Position;
if (volume <= 0m)
volume = _lastLongVolume;
if (volume <= 0m)
return false;
var stopHit = _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value;
var takeHit = _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value;
if (!stopHit && !takeHit)
return false;
var exitPrice = stopHit ? _longStopPrice!.Value : _longTakePrice!.Value;
SellMarket();
var pnl = (exitPrice - _longEntryPrice.Value) * volume;
UpdateMartingale(pnl);
ResetLongState();
return true;
}
private bool TryCloseShort(ICandleMessage candle)
{
if (!_shortEntryPrice.HasValue)
return false;
var volume = Math.Abs(Position);
if (volume <= 0m)
volume = _lastShortVolume;
if (volume <= 0m)
return false;
var stopHit = _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value;
var takeHit = _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value;
if (!stopHit && !takeHit)
return false;
var exitPrice = stopHit ? _shortStopPrice!.Value : _shortTakePrice!.Value;
BuyMarket();
var pnl = (_shortEntryPrice.Value - exitPrice) * volume;
UpdateMartingale(pnl);
ResetShortState();
return true;
}
private decimal CalculateOrderVolume()
{
var minVolume = Security?.MinVolume ?? Volume;
if (minVolume <= 0m)
minVolume = 1m;
var multiplier = UseMartingale ? _martingaleMultiplier : 1m;
var volume = minVolume * multiplier;
return AdjustVolume(volume);
}
private decimal AdjustVolume(decimal volume)
{
var step = Security?.VolumeStep;
if (step.HasValue && step.Value > 0m)
{
var steps = Math.Truncate(volume / step.Value);
volume = steps * step.Value;
}
var min = Security?.MinVolume;
if (min.HasValue && min.Value > 0m && volume < min.Value)
volume = 0m;
var max = Security?.MaxVolume;
if (max.HasValue && max.Value > 0m && volume > max.Value)
volume = max.Value;
return volume;
}
private void UpdateMartingale(decimal realizedPnl)
{
if (!UseMartingale)
{
_martingaleMultiplier = 1m;
return;
}
// Reset the multiplier after profitable trades and scale up after losses.
_martingaleMultiplier = realizedPnl > 0m
? 1m
: _martingaleMultiplier * MartingaleCoefficient;
}
private void ResetLongState()
{
_longEntryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_lastLongVolume = 0m;
}
private void ResetShortState()
{
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_lastShortVolume = 0m;
}
}
import clr
import math
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 precipice_martin_strategy(Strategy):
"""Precipice Martin: grid strategy with alternating long/short entries and martingale sizing."""
def __init__(self):
super(precipice_martin_strategy, self).__init__()
self._use_buy = self.Param("UseBuy", True) \
.SetDisplay("Use Buy", "Enable opening long positions", "Trading")
self._buy_step_pips = self.Param("BuyStepPips", 89) \
.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading")
self._use_sell = self.Param("UseSell", True) \
.SetDisplay("Use Sell", "Enable opening short positions", "Trading")
self._sell_step_pips = self.Param("SellStepPips", 89) \
.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading")
self._use_martingale = self.Param("UseMartingale", True) \
.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing")
self._martingale_coefficient = self.Param("MartingaleCoefficient", 1.6) \
.SetGreaterThanZero() \
.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General")
self._pip_size = 0.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
@property
def UseBuy(self):
return self._use_buy.Value
@property
def BuyStepPips(self):
return int(self._buy_step_pips.Value)
@property
def UseSell(self):
return self._use_sell.Value
@property
def SellStepPips(self):
return int(self._sell_step_pips.Value)
@property
def UseMartingale(self):
return self._use_martingale.Value
@property
def MartingaleCoefficient(self):
return float(self._martingale_coefficient.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _adjust_volume(self, volume):
sec = self.Security
if sec is not None and sec.VolumeStep is not None:
step = float(sec.VolumeStep)
if step > 0:
volume = math.floor(volume / step) * step
if sec is not None and sec.MinVolume is not None:
min_v = float(sec.MinVolume)
if min_v > 0 and volume < min_v:
volume = 0.0
if sec is not None and sec.MaxVolume is not None:
max_v = float(sec.MaxVolume)
if max_v > 0 and volume > max_v:
volume = max_v
return volume
def _calculate_order_volume(self):
sec = self.Security
min_volume = float(sec.MinVolume) if sec is not None and sec.MinVolume is not None else self.Volume
if min_volume <= 0:
min_volume = 1.0
multiplier = self._martingale_multiplier if self.UseMartingale else 1.0
volume = min_volume * multiplier
return self._adjust_volume(volume)
def _update_martingale(self, realized_pnl):
if not self.UseMartingale:
self._martingale_multiplier = 1.0
return
if realized_pnl > 0:
self._martingale_multiplier = 1.0
else:
self._martingale_multiplier *= self.MartingaleCoefficient
def OnStarted2(self, time):
super(precipice_martin_strategy, self).OnStarted2(time)
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
self._pip_size = step * 10.0
if self._pip_size <= 0:
self._pip_size = step if step > 0 else 1.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
close = float(candle.ClosePrice)
closed_long = self._try_close_long(candle)
closed_short = self._try_close_short(candle)
if self.Position != 0:
return
if closed_long or closed_short:
return
if self._long_entry_price is not None or self._short_entry_price is not None:
return
if self.UseBuy and self.UseSell:
if self._prefer_long_entry:
if self._try_enter_long(candle):
self._prefer_long_entry = False
return
if self._try_enter_short(candle):
self._prefer_long_entry = False
else:
if self._try_enter_short(candle):
self._prefer_long_entry = True
return
if self._try_enter_long(candle):
self._prefer_long_entry = True
else:
if self.UseBuy:
self._try_enter_long(candle)
if self.UseSell:
self._try_enter_short(candle)
def _try_enter_long(self, candle):
if self._long_entry_price is not None:
return False
if self.Position != 0:
return False
volume = self._calculate_order_volume()
if volume <= 0:
return False
entry_price = float(candle.ClosePrice)
self.BuyMarket()
self._long_entry_price = entry_price
self._last_long_volume = volume
if self.BuyStepPips > 0:
offset = self.BuyStepPips * self._pip_size
self._long_stop_price = entry_price - offset
self._long_take_price = entry_price + offset
else:
self._long_stop_price = None
self._long_take_price = None
return True
def _try_enter_short(self, candle):
if self._short_entry_price is not None:
return False
if self.Position != 0:
return False
volume = self._calculate_order_volume()
if volume <= 0:
return False
entry_price = float(candle.ClosePrice)
self.SellMarket()
self._short_entry_price = entry_price
self._last_short_volume = volume
if self.SellStepPips > 0:
offset = self.SellStepPips * self._pip_size
self._short_stop_price = entry_price + offset
self._short_take_price = entry_price - offset
else:
self._short_stop_price = None
self._short_take_price = None
return True
def _try_close_long(self, candle):
if self._long_entry_price is None:
return False
volume = self.Position
if volume <= 0:
volume = self._last_long_volume
if volume <= 0:
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
stop_hit = self._long_stop_price is not None and lo <= self._long_stop_price
take_hit = self._long_take_price is not None and h >= self._long_take_price
if not stop_hit and not take_hit:
return False
exit_price = self._long_stop_price if stop_hit else self._long_take_price
self.SellMarket()
pnl = (exit_price - self._long_entry_price) * volume
self._update_martingale(pnl)
self._reset_long_state()
return True
def _try_close_short(self, candle):
if self._short_entry_price is None:
return False
volume = abs(self.Position)
if volume <= 0:
volume = self._last_short_volume
if volume <= 0:
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
stop_hit = self._short_stop_price is not None and h >= self._short_stop_price
take_hit = self._short_take_price is not None and lo <= self._short_take_price
if not stop_hit and not take_hit:
return False
exit_price = self._short_stop_price if stop_hit else self._short_take_price
self.BuyMarket()
pnl = (self._short_entry_price - exit_price) * volume
self._update_martingale(pnl)
self._reset_short_state()
return True
def _reset_long_state(self):
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._last_long_volume = 0.0
def _reset_short_state(self):
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_short_volume = 0.0
def OnReseted(self):
super(precipice_martin_strategy, self).OnReseted()
self._pip_size = 0.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
def CreateClone(self):
return precipice_martin_strategy()