GitHub で見る
フラクチュエート戦略
フラクチュエート戦略は、MetaTraderのエキスパートアドバイザー「Fluctuate」のStockSharpポートです。高レベルAPIを使用してオリジナルのグリッドのような動作を再現します:ローソク足サブスクリプションがすべての決定を駆動し、市場エントリーはBuyMarket / SellMarketで実行され、回復注文はストップ注文で配置されます。ロングとショートのエクスポージャーは、MetaTraderで使用されるヘッジスタイルのポジション会計を模倣するために個別に追跡されますが、実際のStockSharpポジションはネットのままです。
中心的なアイデア
- 新しいローソク足がクローズするたびに、戦略は最後の2つのクローズ価格を比較します。高いクローズは成行買いを開き、低いクローズは成行売りを開きます。両方のクローズが等しい場合、バーは無視されます。
- 各執行ポジションは固定のストップロスとテイクプロフィットを受け取ります(pipsで表現)。戦略はまた正確な執行価格とトレードによって追加された正味ボリュームを記録します。
- エントリーの後、最後の執行価格から
StepPips離れた場所に反対のストップ注文が有効化されます(小さなスプレッドバッファを加えて)。そのボリュームは前のトレードとLotCoefficientから導出され、MultiplyLotCoefficient = trueの場合はオプションで累積エクスポージャーを使用します。
- ストップ注文が発動されると、前の保留注文をキャンセルし、内部エクスポージャー統計を更新し、すぐに他の方向への新しい回復ストップ注文をスケジュールします。これはMQL実装に存在するアベレージング/マーチンゲールループを再現します。
- トレーリング保護は、価格がポジションに有利な方向に少なくとも
TrailingStopPips + TrailingStepPips動くと、ストップを上げます(または下げます)。これはストップを締める前に追加の利益バッファが必要だった元のEAをエミュレートします。
トレードワークフロー
- シグナル検出。 ローソク足フィードは
SubscribeCandlesでサブスクライブされます。完了したローソク足のみが処理されます。戦略は[StartHour, EndHour)時間ウィンドウ外またはエクイティガードが発動されたときはトレードを拒否します。
- 初期ポジションサイジング。
PositionSizingModeに応じて、シーケンスの最初のトレードは固定ロット(FixedVolume)またはリスクベースのロット(RiskPercent)を使用します。リスクモードでは、許可されたリスク(現在のエクイティのパーセンテージ)はストップロスが発動された場合に発生する金銭的損失で割られます。価格ステップとステップ価格はpipsを通貨に変換するために使用されます。
- エクスポージャー会計。 個別のアキュムレーターがロングとショートのボリューム、平均価格、エントリー以来達成した極価格を追跡します。これにより、StockSharpがネッティングを使用していても、戦略は両サイドを内部的に「オープン」に保つことができます。
- 回復注文。 各執行後、アルゴリズムは次のストップ注文のボリュームを計算します:
MultiplyLotCoefficient = falseの場合、新しいボリュームはLastVolume × LotCoefficientに等しい。
trueの場合、総絶対エクスポージャーがLotCoefficientで乗算されます。
- ボリュームは取引所の制約(ステップ、最小・最大ボリューム)に正規化され、
MaxTotalVolumeを超えるか、アクティブポジションと注文の数がMaxPositionsを超える場合に拒否されます。
- 利益目標とエクイティガード。 集計された未実現PnLは
PriceStep/StepPriceを使用して価格差を通貨に変換することで計算されます。ProfitTargetに達すると、すべてのポジションが決済されて保留注文がキャンセルされます。エクイティが初期残高のMinEquityPercentを下回ると、トレーディングも停止されます。
- トレーリングロジック。 ロングポジションの場合、エントリー以来見た最高価格が記録されます。エントリー価格を
TrailingStopPips + TrailingStepPips上回ると、高値の後ろTrailingStopPipsにトレーリングストップが設定されます。ショートポジションは最低価格で対称的なルールを適用します。トレーリング更新は固定ストップロスを上書きします。
リスク管理の詳細
- ストップ/テイクプロフィット。 どちらもオプションです(pip値をゼロに設定して無効化)。新しいトレードがボリュームを追加するたびに、集計されたロングまたはショートエクスポージャーに対して再計算されます。
- 最大ポジション数。 オープンサイドの数(ロング + ショート)と現在の回復ストップ注文を数えます。限界に達すると、戦略は新しいストップ注文の提出を拒否します。
- 最大総ボリューム。 絶対オープンボリュームとアクティブ回復注文のボリュームの合計を制限します。
- CloseAllAtStart。 戦略がトレードを開始する前にブックをフラットにするオプションの安全スイッチ。
パラメーター
| 名前 |
説明 |
デフォルト |
CandleType |
シグナル検出に使用される主要な時間軸。 |
1分の時間軸 |
StopLossPips |
エントリー価格とストップロスの間の距離(pips)。0はストップを無効にします。 |
50 |
TakeProfitPips |
エントリー価格とテイクプロフィットの間の距離(pips)。0はテイクプロフィットを無効にします。 |
50 |
TrailingStopPips |
トレーリングストップ距離(pips)。TrailingStepPips > 0が必要です。 |
5 |
TrailingStepPips |
トレーリングストップが進む前に必要な追加利益(pips)。 |
5 |
StepPips |
最後の執行と反対の回復ストップの間の距離(pips)。 |
30 |
LotCoefficient |
前のボリューム(または総エクスポージャー)に適用される乗数。 |
2.0 |
MultiplyLotCoefficient |
trueの場合、新しい注文ボリュームは最後のトレードではなく総エクスポージャーから計算されます。 |
false |
MaxPositions |
同時オープンサイドの最大数とアクティブ保留注文。 |
9 |
MaxTotalVolume |
オープンボリュームと回復注文ボリュームの合計の上限。 |
50 |
ProfitTarget |
完全なエグジットを引き起こす未実現利益(口座通貨)。0は目標を無効にします。 |
50 |
MinEquityPercent |
トレーディングを続けるために必要な最小エクイティパーセンテージ(初期残高比)。この閾値を下回ると、エグジットのみ許可されます。 |
30 |
CloseAllAtStart |
戦略が開始するときにすべてのポジションを決済して注文をキャンセルします。 |
false |
StartHour |
トレーディングウィンドウ開始時間(含む、取引所時間)。 |
10 |
EndHour |
トレーディングウィンドウ終了時間(含まない、取引所時間)。 |
20 |
PositionSizingMode |
静的ロットのFixedVolume、またはエクイティパーセントサイジングのRiskPercent。 |
FixedVolume |
VolumeOrRisk |
固定ロットサイズ(FixedVolumeの場合)またはリスクパーセンテージ(RiskPercentの場合)。 |
1.0 |
実装上の注意
- ストップ注文の価格は最小スプレッド近似を使用します(
PriceStepが利用可能な場合)。MetaTraderがフリーズレベルの外に注文を置くことを必要としたためです。実際のスプレッドが広い場合はStepPipsを調整してください。
- 戦略は新しいトレードが執行されるたびに残りの回復注文をキャンセルします。これは執行後にすべての保留注文を削除した元のEAに一致します。
- StockSharpポートフォリオはネッティングされているため、ヘッジエクスポージャーは内部でシミュレートされます。実際のブローカーポジションは常に正味数量を反映します。
- リスクベースのポジションサイジングは銘柄説明からの有効な
PriceStepとStepPriceの値が必要です。
使用上のヒント
- 最良の忠実度のために、元のEAテスト時間軸(通常M5またはM15)に一致する適切なローソク足タイプを選択します。
- 取引所ボリュームの制限を再確認します:正規化された回復ボリュームがゼロになると、戦略は新しいレッグの追加を停止します。
PositionSizingMode = RiskPercentの場合、ポートフォリオに最新のエクイティ情報が含まれていることを確認します;そうでなければ戦略は固定ロットサイズにフォールバックします。
- 必要に応じてアカウントレベルの追加の保護を追加するために、StockSharpの組み込み
StrategyProtection(StartProtection()で有効化)と組み合わせます。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Fluctuate strategy using EMA crossover with stop-loss and take-profit.
/// Buys when fast EMA crosses above slow EMA, sells on reverse cross.
/// </summary>
public class FluctuateStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initialize <see cref="FluctuateStrategy"/>.
/// </summary>
public FluctuateStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_slow = null;
_prevFast = 0;
_prevSlow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_slow.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 50;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
// EMA crossover buy signal
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 50;
}
// EMA crossover sell signal
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 50;
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class fluctuate_strategy(Strategy):
def __init__(self):
super(fluctuate_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 50) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(fluctuate_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(fluctuate_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# EMA crossover buy signal
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 50
# EMA crossover sell signal
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return fluctuate_strategy()