Amstell グリッド・マネージャー戦略
双方向の平均化グリッドを実行するMetaTraderエキスパート「exp_Amstell-SL」の高レベルポートです。戦略は各サイドの最新の約定価格を追跡し、価格が十分に離れたときに追加の成行注文を発し、固定のテイクプロフィットまたはストップロス距離に達したときにオープンバッチを清算します。実装はStockSharpのローソク足サブスクリプションと高レベル注文ヘルパーを使用しているため、単一の銘柄のローソク足データを提供する任意の環境に接続できます。
翻訳されたロジックはStockSharpのネットポートフォリオモデルに若干適応されています。ロングとショートのグリッドは依然として別々に管理されますが、同時には保持されません。ロンググリッドはネットポジションが非負の間アクティブであり、すべてのロングエクスポージャーが平坦化された後にのみショートグリッドが引き継ぎます。
動作方法
マーケットデータと実行フロー
- 設定された
CandleType(デフォルト: 1分足ローソク足)をサブスクライブし、完成したローソク足のみを処理します。 - 銘柄の
PriceStepからpipsベースのオフセットを計算します。ステップが3桁または5桁の場合、MetaTraderの3/5桁pip調整を模倣するために10倍されます。 - すべての取引は
BuyMarket/SellMarketヘルパーを通じて配置されます。ペンディング注文は使用しません。
ロングサイドの管理
- 既存のロングエクスポージャーがなく、戦略がショートを閉じる最中でない場合に最初のロングポジション(
OrderVolume)を開きます。 - 最新のロング約定価格とアクティブなロングバッチの出来高加重平均エントリー価格を追跡します。
- 終値が最後のロング約定価格より少なくとも
BuyDistancePips(価格単位に変換)下落した場合、常にOrderVolumeサイズの追加ロング注文を発します。
ショートサイドの管理
- ロングバッチが完全に閉じられ、ネットポジションが非正になったら、戦略はショートエントリーを許可します。
- ショートエクスポージャーがない場合に最初のショート注文を発します。価格が前のショート約定価格より
BuyDistancePips * SellDistanceMultiplier上昇した後に追加のショートが開かれます。 - アクティブなショートバッチの最新のショート約定価格と出来高加重平均エントリー価格を維持します。
エグジットルール
- 各方向について、平均約定に対する未実現利益を計算します。
- 利益が
TakeProfitPipspipsに達するか、ドローダウンがStopLossPipspipsに達したときに、成行売りでロングバッチ全体を閉じます。 - 利益が
TakeProfitPipspipsに達するか、不利な動きがStopLossPipspipsに達したときに、成行買いでショートバッチ全体を閉じます。 - 清算後、キャッシュされたすべての価格とボリュームがリセットされ、次のローソク足で新しいグリッドを開始できます。
オリジナルMQLエキスパートとの違い
- StockSharpバージョンは個別のティックではなくローソク足のクローズで動作します。
- ロングとショートのグリッドはStockSharpのデフォルトネッティングモードに合わせて、同時ではなく順次実行されます。
- すべての保護距離は各チケットを個別にではなく平均エントリー価格に対して確認され、集計されたネットポジションの動作を反映します。
パラメーター
| パラメーター | デフォルト | 最適化範囲 | 説明 |
|---|---|---|---|
OrderVolume |
0.01 |
0.01 – 0.10 (ステップ 0.01) |
各グリッド注文で送信される数量。正である必要があります。 |
TakeProfitPips |
30 |
10 – 150 (ステップ 10) |
アクティブバッチの利益目標(pips単位)。 |
StopLossPips |
30 |
10 – 150 (ステップ 10) |
バッチを諦める前の最大不利な動き。 |
BuyDistancePips |
10 |
5 – 60 (ステップ 5) |
別の買いを追加するための最後のロング約定からの最小下落。TPとSLの両方より小さい必要があります。 |
SellDistanceMultiplier |
10 |
2 – 15 (ステップ 1) |
ショートエントリーを間隔を置く際のロング距離に適用される乗数。 |
CandleType |
1分足時間軸 | — | シグナル生成に使用するローソク足シリーズ。 |
実装に関する注意事項
BuyDistancePipsはTakeProfitPipsとStopLossPipsの両方より厳密に小さくなければなりません。そうでない場合、戦略は起動時に例外をスローし、MetaTraderの検証を再現します。- pip サイズは銘柄の
PriceStepから導出されます。楽器が非標準のティックサイズを使用している場合はパラメーターを調整してください。 - すべての内部状態は
OnResetedでクリアされ、残りのグリッドデータなしに戦略を再起動できます。 - カラーカスタマイズや手動のインジケーター登録は使用されず、このリポジトリの高レベルAPIガイドラインに準拠しています。
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>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _buyDistancePips;
private readonly StrategyParam<decimal> _sellDistanceMultiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _longVolume;
private decimal _shortVolume;
private decimal? _averageLongPrice;
private decimal? _averageShortPrice;
private decimal? _lastBuyPrice;
private decimal? _lastSellPrice;
private decimal _pipValue;
private decimal _takeProfitOffset;
private decimal _stopLossOffset;
private decimal _buyDistanceOffset;
private decimal _sellDistanceOffset;
private bool _closingLong;
private bool _closingShort;
/// <summary>
/// Quantity per market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Profit target in pips for each grid leg.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Maximum tolerated loss in pips for each grid leg.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Price distance in pips required to add another long position.
/// </summary>
public int BuyDistancePips
{
get => _buyDistancePips.Value;
set => _buyDistancePips.Value = value;
}
/// <summary>
/// Multiplier applied to the long distance when stacking short entries.
/// </summary>
public decimal SellDistanceMultiplier
{
get => _sellDistanceMultiplier.Value;
set => _sellDistanceMultiplier.Value = value;
}
/// <summary>
/// Candle data type used for decision making.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes the strategy parameters.
/// </summary>
public AmstellGridManagerStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
.SetOptimize(0.01m, 0.1m, 0.01m);
_takeProfitPips = Param(nameof(TakeProfitPips), 30)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
.SetOptimize(10, 150, 10);
_stopLossPips = Param(nameof(StopLossPips), 30)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
.SetOptimize(10, 150, 10);
_buyDistancePips = Param(nameof(BuyDistancePips), 10)
.SetGreaterThanZero()
.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
.SetOptimize(5, 60, 5);
_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
.SetGreaterThanZero()
.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
.SetOptimize(2m, 15m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for processing", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longVolume = 0m;
_shortVolume = 0m;
_averageLongPrice = null;
_averageShortPrice = null;
_lastBuyPrice = null;
_lastSellPrice = null;
_pipValue = 0m;
_takeProfitOffset = 0m;
_stopLossOffset = 0m;
_buyDistanceOffset = 0m;
_sellDistanceOffset = 0m;
_closingLong = false;
_closingShort = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");
UpdatePriceOffsets();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// no indicators bound via .Bind()
var close = candle.ClosePrice;
if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
{
var profit = close - longAvg;
if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
{
SellMarket();
_closingLong = true;
return;
}
}
if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
{
var profit = shortAvg - close;
if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
{
BuyMarket();
_closingShort = true;
return;
}
}
var openedLong = false;
if (!_closingLong && Position >= 0m)
{
if (_longVolume <= 0m)
{
BuyMarket();
openedLong = true;
}
else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
{
BuyMarket();
openedLong = true;
}
}
if (openedLong)
return;
if (!_closingShort && Position <= 0m)
{
if (_shortVolume <= 0m)
{
SellMarket();
}
else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
{
SellMarket();
}
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null)
return;
var tradeVolume = trade.Trade.Volume;
var price = trade.Trade.Price;
if (trade.Order.Side == Sides.Buy)
{
if (_shortVolume > 0m)
{
var closingVolume = Math.Min(tradeVolume, _shortVolume);
_shortVolume -= closingVolume;
tradeVolume -= closingVolume;
if (_shortVolume <= 0m)
{
_shortVolume = 0m;
_averageShortPrice = null;
_lastSellPrice = null;
}
}
if (tradeVolume > 0m)
{
var newVolume = _longVolume + tradeVolume;
var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
_longVolume = newVolume;
_averageLongPrice = totalCost / newVolume;
_lastBuyPrice = price;
_closingLong = false;
}
}
else if (trade.Order.Side == Sides.Sell)
{
if (_longVolume > 0m)
{
var closingVolume = Math.Min(tradeVolume, _longVolume);
_longVolume -= closingVolume;
tradeVolume -= closingVolume;
if (_longVolume <= 0m)
{
_longVolume = 0m;
_averageLongPrice = null;
_lastBuyPrice = null;
}
}
if (tradeVolume > 0m)
{
var newVolume = _shortVolume + tradeVolume;
var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
_shortVolume = newVolume;
_averageShortPrice = totalCost / newVolume;
_lastSellPrice = price;
_closingShort = false;
}
}
if (_longVolume <= 0m && Position <= 0m)
_closingLong = false;
if (_shortVolume <= 0m && Position >= 0m)
_closingShort = false;
if (Position == 0m)
{
_longVolume = 0m;
_shortVolume = 0m;
_averageLongPrice = null;
_averageShortPrice = null;
_lastBuyPrice = null;
_lastSellPrice = null;
_closingLong = false;
_closingShort = false;
}
}
private void UpdatePriceOffsets()
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
step = 1m;
var decimals = GetDecimalPlaces(step);
_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;
_takeProfitOffset = TakeProfitPips * _pipValue;
_stopLossOffset = StopLossPips * _pipValue;
_buyDistanceOffset = BuyDistancePips * _pipValue;
_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
}
private static int GetDecimalPlaces(decimal value)
{
if (value == 0m)
return 0;
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0x7F;
}
}
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 amstell_grid_manager_strategy(Strategy):
"""Amstell averaging grid strategy with TP/SL and distance-based grid entries."""
def __init__(self):
super(amstell_grid_manager_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
self._take_profit_pips = self.Param("TakeProfitPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._buy_distance_pips = self.Param("BuyDistancePips", 10) \
.SetGreaterThanZero() \
.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
self._sell_distance_mult = self.Param("SellDistanceMultiplier", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance for shorts", "Entries")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for processing", "General")
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def BuyDistancePips(self):
return self._buy_distance_pips.Value
@property
def SellDistanceMultiplier(self):
return self._sell_distance_mult.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_value(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 1.0
step = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(amstell_grid_manager_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
self._pip_value = self._calc_pip_value()
self._tp_offset = self.TakeProfitPips * self._pip_value
self._sl_offset = self.StopLossPips * self._pip_value
self._buy_dist_offset = self.BuyDistancePips * self._pip_value
self._sell_dist_offset = self._buy_dist_offset * float(self.SellDistanceMultiplier)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
# Check long TP/SL
if not self._closing_long and self._long_volume > 0 and self._avg_long_price is not None:
profit = close - self._avg_long_price
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.SellMarket()
self._closing_long = True
return
# Check short TP/SL
if not self._closing_short and self._short_volume > 0 and self._avg_short_price is not None:
profit = self._avg_short_price - close
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.BuyMarket()
self._closing_short = True
return
opened_long = False
# Grid long entries
if not self._closing_long and self.Position >= 0:
if self._long_volume <= 0:
self.BuyMarket()
self._record_buy(close)
opened_long = True
elif self._last_buy_price is not None and self._last_buy_price - close >= self._buy_dist_offset:
self.BuyMarket()
self._record_buy(close)
opened_long = True
if opened_long:
return
# Grid short entries
if not self._closing_short and self.Position <= 0:
if self._short_volume <= 0:
self.SellMarket()
self._record_sell(close)
elif self._last_sell_price is not None and close - self._last_sell_price >= self._sell_dist_offset:
self.SellMarket()
self._record_sell(close)
def _record_buy(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._long_volume + vol
total_cost = (self._avg_long_price if self._avg_long_price is not None else 0.0) * self._long_volume + price * vol
self._long_volume = new_vol
self._avg_long_price = total_cost / new_vol if new_vol > 0 else price
self._last_buy_price = price
self._closing_long = False
def _record_sell(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._short_volume + vol
total_cost = (self._avg_short_price if self._avg_short_price is not None else 0.0) * self._short_volume + price * vol
self._short_volume = new_vol
self._avg_short_price = total_cost / new_vol if new_vol > 0 else price
self._last_sell_price = price
self._closing_short = False
def _check_position_sync(self):
if self.Position == 0:
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._closing_long = False
self._closing_short = False
def OnReseted(self):
super(amstell_grid_manager_strategy, self).OnReseted()
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
def CreateClone(self):
return amstell_grid_manager_strategy()