ホーム
/
戦略のサンプル
GitHub で見る
ターングリッド戦略
概要
The TurnGrid Strategy replicates the behaviour of the original MQL5 Expert Advisor TurnGrid.mq5.現在の市場価格を中心に対称的な価格グリッドを構築し、価格が 1 つのグリッド セルから別のグリッド セルに移動するたびにロング注文とショート注文を交互に切り替えます。 The strategy continuously rebalances open orders to maintain both bullish and bearish exposure until the configured equity target is achieved.
変換には、StockSharp の高レベルの API が使用されます。つまり、ローソク足のサブスクリプションがグリッドの更新を駆動し、成行注文がエントリーとエグジットを処理し、リスク管理が戦略パラメーターを通じて表現されます。 All comments have been translated into English and the naming follows StockSharp conventions.
取引ロジック
When the strategy starts it captures the latest candle close and builds a grid containing 4 * GridShares levels. The central level is set to the current price, upper levels scale by 1 + GridDistance, and lower levels scale by 1 - GridDistance.
An initial market buy order is placed at the centre of the grid.そのボリュームは、利用可能な予算部分 (Balance / GridShares) と、MQL バージョンから継承された増分ステーク式から計算されます。
Every finished candle updates the current grid index based on the close price.インデックスが変更された場合:
新しいインデックスから 2 レベル離れたチケットにリンクされたポジションは閉じられます (価格より低い購入チケットは売却され、より高い価格で販売されたチケットは買い戻されます)。
New positions are opened to keep both long and short anchors on the active level. If neither side is present, the strategy opens the side with fewer active positions to balance exposure.
Fees are approximated via the FeeRate parameter. Each filled order contributes to a running fee total used when evaluating performance.
アカウントの資本(累積手数料推定値を差し引いた後)が初期残高を EquityTakeProfit 超えると、ストラテジーはネットポジションを閉じ、最新の価格を中心にグリッドを再構築します。
パラメーター
名前
説明
デフォルト
GridDistance
隣接するグリッド レベル間の相対距離。
0.01
GridShares
Maximum number of concurrent grid positions that can be active.
50
EquityTakeProfit
Percentage gain over the initial balance required to reset the grid.
0.02
FeeRate
Estimated transaction fee per trade, applied to executed volume.
0.0008
CandleType
戦略を推進するために使用されたキャンドル シリーズ。
1 分の時間枠
実装メモ
キャンドルのサブスクリプションは SubscribeCandles(CandleType) を介して処理され、戦略は終了したキャンドルにのみ反応し、StockSharp との互換性を維持しながら、元の EA のティック駆動ロジックと一致します。
グリッドの状態は、価格アンカー、ブール値フラグ、遅延クロージャのチケット ボリュームを含む GridLevel 構造体の軽量配列に保存されます。
注文サイズは元の増分資本配分式に従い、証券の VolumeStep、VolumeMin、および VolumeMax 設定による追加の正規化が行われます。
Equity-based resets wait for the current net position to close before rebuilding the grid, ensuring clean transitions between trading cycles.
ファイル
CS/TurnGridStrategy.cs – C# implementation of the strategy using StockSharp high-level constructs.
README.md – 英語のドキュメント (このファイル)。
README_zh.md – 簡体字中国語のドキュメント。
README_ru.md – ロシア語のドキュメント。
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 trading strategy that mirrors the TurnGrid Expert Advisor logic from MQL5.
/// </summary>
public class TurnGridStrategy : Strategy
{
private enum TradeDirections
{
Buy,
Sell,
}
private struct GridLevel
{
public decimal Price;
public bool HasBuy;
public bool HasSell;
public decimal BuyVolumeTicket;
public decimal SellVolumeTicket;
}
private readonly StrategyParam<decimal> _gridDistance;
private readonly StrategyParam<int> _gridShares;
private readonly StrategyParam<decimal> _equityTakeProfit;
private readonly StrategyParam<decimal> _feeRate;
private readonly StrategyParam<DataType> _candleType;
private GridLevel[] _grid;
private int _currentIndex;
private decimal _openBudget;
private decimal _openMoneyIncrement;
private int _buyCount;
private int _sellCount;
private decimal _lastPrice;
private decimal _totalFee;
private decimal _initialBalance;
private bool _resetRequested;
private decimal _resetPrice;
public decimal GridDistance
{
get => _gridDistance.Value;
set => _gridDistance.Value = value;
}
public int GridShares
{
get => _gridShares.Value;
set => _gridShares.Value = value;
}
public decimal EquityTakeProfit
{
get => _equityTakeProfit.Value;
set => _equityTakeProfit.Value = value;
}
public decimal FeeRate
{
get => _feeRate.Value;
set => _feeRate.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TurnGridStrategy()
{
_gridDistance = Param(nameof(GridDistance), 0.01m)
.SetDisplay("Grid Distance", "Relative distance between grid levels", "Grid");
_gridShares = Param(nameof(GridShares), 50)
.SetDisplay("Max Grid Positions", "Maximum number of open grid entries", "Grid");
_equityTakeProfit = Param(nameof(EquityTakeProfit), 0.02m)
.SetDisplay("Equity Take Profit", "Equity growth ratio that triggers a reset", "Risk");
_feeRate = Param(nameof(FeeRate), 0.0008m)
.SetDisplay("Fee Rate", "Estimated transaction fee per trade", "Costs");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used to drive the grid", "Data");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_grid = null;
_currentIndex = 0;
_openBudget = 0m;
_openMoneyIncrement = 0m;
_buyCount = 0;
_sellCount = 0;
_lastPrice = 0m;
_totalFee = 0m;
_initialBalance = 0m;
_resetRequested = false;
_resetPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
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;
_lastPrice = candle.ClosePrice;
if (_resetRequested)
{
if (!TryCloseNetPosition())
return;
InitializeGrid(_resetPrice);
_resetRequested = false;
}
if (_grid == null)
{
InitializeGrid(candle.ClosePrice);
return;
}
if (!UpdateCurrentIndex(candle.ClosePrice))
return;
if (CheckEquityTarget())
{
RequestReset(candle.ClosePrice);
return;
}
CloseReachedPositions();
ManageOpenings();
}
private void InitializeGrid(decimal price)
{
if (price <= 0m)
return;
var shares = Math.Max(1, GridShares);
var size = shares * 4;
_grid = new GridLevel[size];
_currentIndex = shares * 2;
_grid[_currentIndex].Price = price;
for (var i = _currentIndex + 1; i < size; i++)
{
_grid[i].Price = _grid[i - 1].Price * (1m + GridDistance);
}
for (var i = _currentIndex - 1; i >= 0; i--)
{
_grid[i].Price = _grid[i + 1].Price * (1m - GridDistance);
}
_buyCount = 0;
_sellCount = 0;
_totalFee = 0m;
var portfolio = Portfolio;
_initialBalance = portfolio?.CurrentValue ?? portfolio?.CurrentValue ?? _initialBalance;
if (_initialBalance <= 0m)
_initialBalance = shares * price;
_openBudget = _initialBalance / shares;
if (_openBudget <= 0m)
_openBudget = price;
_openMoneyIncrement = CalculateOpenMoneyIncrement();
_lastPrice = price;
TryOpenBuy();
}
private bool UpdateCurrentIndex(decimal price)
{
if (_grid == null)
return false;
var newIndex = _currentIndex;
while (newIndex + 1 < _grid.Length && price >= _grid[newIndex + 1].Price)
newIndex++;
while (newIndex - 1 >= 0 && price <= _grid[newIndex - 1].Price)
newIndex--;
if (newIndex == _currentIndex)
return false;
_currentIndex = newIndex;
return true;
}
private bool CheckEquityTarget()
{
if (_initialBalance <= 0m)
return false;
var portfolio = Portfolio;
var equity = portfolio?.CurrentValue ?? portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return false;
return equity - _totalFee > _initialBalance * (1m + EquityTakeProfit);
}
private void RequestReset(decimal price)
{
_resetRequested = true;
_resetPrice = price;
_grid = null;
_buyCount = 0;
_sellCount = 0;
_totalFee = 0m;
TryCloseNetPosition();
}
private bool TryCloseNetPosition()
{
if (Position > 0m)
{
SellMarket(Position);
return false;
}
if (Position < 0m)
{
BuyMarket(Math.Abs(Position));
return false;
}
return true;
}
private void CloseReachedPositions()
{
if (_grid == null)
return;
ref var currentLevel = ref _grid[_currentIndex];
if (currentLevel.BuyVolumeTicket > 0m)
{
SellMarket(currentLevel.BuyVolumeTicket);
_buyCount = Math.Max(0, _buyCount - 1);
currentLevel.BuyVolumeTicket = 0m;
var anchorIndex = _currentIndex - 2;
if (anchorIndex >= 0)
_grid[anchorIndex].HasBuy = false;
}
if (currentLevel.SellVolumeTicket > 0m)
{
BuyMarket(currentLevel.SellVolumeTicket);
_sellCount = Math.Max(0, _sellCount - 1);
currentLevel.SellVolumeTicket = 0m;
var anchorIndex = _currentIndex + 2;
if (_grid != null && anchorIndex < _grid.Length)
_grid[anchorIndex].HasSell = false;
}
}
private void ManageOpenings()
{
if (_grid == null)
return;
ref var level = ref _grid[_currentIndex];
if (level.HasBuy && !level.HasSell)
{
TryOpenSell();
return;
}
if (!level.HasBuy && level.HasSell)
{
TryOpenBuy();
return;
}
if (!level.HasBuy && !level.HasSell)
{
if (_buyCount > _sellCount)
TryOpenSell();
else
TryOpenBuy();
}
}
private void TryOpenBuy()
{
if (_grid == null)
return;
if (_buyCount + _sellCount >= GridShares)
return;
var volume = CalculateVolume(TradeDirections.Buy);
if (volume <= 0m)
return;
BuyMarket(volume);
ref var level = ref _grid[_currentIndex];
level.HasBuy = true;
var targetIndex = _currentIndex + 2;
if (targetIndex < _grid.Length)
_grid[targetIndex].BuyVolumeTicket += volume;
_buyCount++;
}
private void TryOpenSell()
{
if (_grid == null)
return;
if (_buyCount + _sellCount >= GridShares)
return;
var volume = CalculateVolume(TradeDirections.Sell);
if (volume <= 0m)
return;
SellMarket(volume);
ref var level = ref _grid[_currentIndex];
level.HasSell = true;
var targetIndex = _currentIndex - 2;
if (targetIndex >= 0)
_grid[targetIndex].SellVolumeTicket += volume;
_sellCount++;
}
private decimal CalculateVolume(TradeDirections direction)
{
if (_lastPrice <= 0m)
return 0m;
var firstMoney = _openBudget / 10m;
if (firstMoney <= 0m)
firstMoney = _lastPrice;
decimal money;
switch (direction)
{
case TradeDirections.Buy:
money = firstMoney + _buyCount * _openMoneyIncrement;
break;
case TradeDirections.Sell:
money = firstMoney + _sellCount * _openMoneyIncrement;
break;
default:
money = firstMoney;
break;
}
if (money <= 0m)
return 0m;
var volume = money / _lastPrice;
volume = NormalizeVolume(volume);
if (volume <= 0m)
return 0m;
_totalFee += _lastPrice * volume * FeeRate;
LogInfo($"Total Fee = {_totalFee:F2}; Grid = {_buyCount + _sellCount} / {GridShares} ({_buyCount}, {_sellCount})");
return volume;
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
volume = step * Math.Round(volume / step, MidpointRounding.AwayFromZero);
var min = 0m;
if (min > 0m && volume < min)
return 0m;
var max = decimal.MaxValue;
if (volume > max)
volume = max;
return volume;
}
private decimal CalculateOpenMoneyIncrement()
{
var halfShares = GridShares / 2m;
if (halfShares <= 1m)
return 0m;
var numerator = _initialBalance / 2m - halfShares / 10m;
if (numerator <= 0m)
numerator = _initialBalance / 4m;
var denominator = halfShares * (halfShares - 1m) / 2m;
if (denominator <= 0m)
return 0m;
return numerator / denominator;
}
}
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 SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class turn_grid_strategy(Strategy):
"""Simplified grid: SMA crossover (10/30) for direction with alternating trades."""
def __init__(self):
super(turn_grid_strategy, self).__init__()
self._grid_dist = self.Param("GridDistance", 0.01).SetDisplay("Grid Distance", "Relative distance between levels", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Candle type", "Data")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(turn_grid_strategy, self).OnReseted()
self._prev_fast = 0
self._prev_slow = 0
self._last_trade_price = 0
def OnStarted2(self, time):
super(turn_grid_strategy, self).OnStarted2(time)
self._prev_fast = 0
self._prev_slow = 0
self._last_trade_price = 0
fast = SimpleMovingAverage()
fast.Length = 10
slow = SimpleMovingAverage()
slow.Length = 30
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
close = float(candle.ClosePrice)
dist = self._grid_dist.Value
if self._prev_fast == 0 or self._prev_slow == 0:
self._prev_fast = fast
self._prev_slow = slow
self._last_trade_price = close
return
# Grid re-entry check
if self._last_trade_price > 0 and dist > 0:
price_move = abs(close - self._last_trade_price) / self._last_trade_price
if price_move < dist:
self._prev_fast = fast
self._prev_slow = slow
return
cross_up = self._prev_fast <= self._prev_slow and fast > slow
cross_down = self._prev_fast >= self._prev_slow and fast < slow
if cross_up and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._last_trade_price = close
elif cross_down and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._last_trade_price = close
self._prev_fast = fast
self._prev_slow = slow
def CreateClone(self):
return turn_grid_strategy()