インデックステスター戦略
概要
Indices Tester Strategy は、MetaTrader 5 エキスパート アドバイザー「Indices Tester」を直接移植したものです。このシステムは、非常に狭い取引ウィンドウで単一のロングポジションがオープンされる日中のインデックス取引に焦点を当てています。取引の決定は純粋に時間フィルターと運用制限に依存します。
- 単一の構成可能なローソク足ストリームが戦略の内部クロックを駆動します。
- 新しいポジションは、設定されたセッションの開始時間と終了時間の間にのみオープンできます。
- 1 日に許可される取引回数は固定されており、繰り返しの再エントリーを防ぎます。
- すべてのオープンポジションは、定義された清算時間に強制的にクローズされます。
- この戦略はロングサイドのみで動作し、元のエキスパートアドバイザーを反映しています。
この実装では、高レベルの StockSharp API を使用し、SubscribeCandles でローソク足データをサブスクライブし、ProcessCandle コールバックで取引の決定を処理します。インジケーターは必要ないため、ロジックはスリムに保たれ、タイミングとリスクの管理に重点が置かれます。
取引ロジック
- 毎日リセット – この戦略は現在の取引日を追跡します。新しい日が始まるとすべてのカウンターがリセットされ、その日の新たな取引枠が可能になります。
- エントリーウィンドウ – 厳密に
[SessionStart, SessionEnd) 間隔内の終値を持つローソク足のみがエントリーをトリガーできます。これにより、元のコードの TimeStart チェックと TimeEnd チェックが再現されます。
- ポジションと取引の制限 – その日の間にすでにオープンされた取引の数が
DailyTradeLimit に達した場合、または同時にオープンされたポジションの数が MaxOpenPositions を超えた場合、エントリーはスキップされます。
- 注文送信 – すべての条件が戦略に合致した場合、
TradeVolume ユニットの成行買い注文を送信します。その日の取引カウンターは注文送信直後に増加します。
- 強制終了 – ローソク足が
CloseTime の後にクローズし、アクティブなロングポジションがある場合、戦略は成行売り注文でポジションをクローズします。これは、MQL 実装の ClosePos() タイマー ロジックを反映しています。
取引カウンタとポジション リミッタの組み合わせにより、システムはデフォルトで単純な 1 日あたり 1 回の取引スケジューラとして動作することが保証されますが、より頻繁なアクティビティに合わせてパラメータを調整することもできます。
パラメーター
| 名前 |
説明 |
CandleType |
戦略クロックを駆動する主要なローソク足シリーズ (デフォルトは 1 分ローソク足)。 |
SessionStart |
新しい取引の開始が許可される時刻。 |
SessionEnd |
新規取引が禁止される時間帯。 |
CloseTime |
残りのオープンポジションが清算される時刻。 |
DailyTradeLimit |
取引が停止されるまでに許可される 1 日あたりのエントリーの最大数。 |
MaxOpenPositions |
同時にオープンするロングポジションの最大数(取引単位でカウント)。 |
TradeVolume |
Market order volume used for each entry. |
注意事項と相違点
- StockSharp は MetaTrader セッション テーブルを公開しないため、変換は
IsFormedAndOnlineAndAllowTrading() ガードとともにローソク足のタイムスタンプからの交換時間に依存します。
- 元の Expert Advisor は第 2 レベルのタイマーを使用していました。このポートはローソク足の終値を利用してエントリーのタイミングと強制退出の両方を推進します。これは分単位の取引ウィンドウには十分です。
- 取引カウントは、ローソク足の終了時刻から検出された各取引日の開始時にリセットされ、ローソク足のソースが目的の為替と一致する限り、異なるタイムゾーン間でも動作の一貫性を保ちます。
使用のヒント
- 時間フィルターが目的のセッションと一致するように、構成された
CandleType が取引されている市場と一致していることを確認してください。
- 短い時間枠で実行する場合など、1 日に複数回の試行が必要な場合は、
DailyTradeLimit を増やします。
- 位置への部分的なスケーリングが必要な場合にのみ、
MaxOpenPositions を 1 よりも上に設定します。それ以外の場合は、デフォルトのままにして、MetaTrader スクリプトを正確に模倣します。
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>
/// Port of the MetaTrader expert advisor "Indices Tester".
/// Implements a time filtered long-only session with daily trade and position limits.
/// </summary>
public class IndicesTesterStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<TimeSpan> _sessionStart;
private readonly StrategyParam<TimeSpan> _sessionEnd;
private readonly StrategyParam<TimeSpan> _closeTime;
private readonly StrategyParam<int> _dailyTradeLimit;
private readonly StrategyParam<int> _maxOpenPositions;
private readonly StrategyParam<decimal> _tradeVolume;
private DateTime _currentDay;
private int _tradesOpenedToday;
/// <summary>
/// Candle type used to drive the strategy clock.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Session start time when new long positions may be opened.
/// </summary>
public TimeSpan SessionStart
{
get => _sessionStart.Value;
set => _sessionStart.Value = value;
}
/// <summary>
/// Session end time after which new positions are not allowed.
/// </summary>
public TimeSpan SessionEnd
{
get => _sessionEnd.Value;
set => _sessionEnd.Value = value;
}
/// <summary>
/// Time of day when all active positions are closed.
/// </summary>
public TimeSpan CloseTime
{
get => _closeTime.Value;
set => _closeTime.Value = value;
}
/// <summary>
/// Maximum number of entries that can be opened during a single trading day.
/// </summary>
public int DailyTradeLimit
{
get => _dailyTradeLimit.Value;
set => _dailyTradeLimit.Value = value;
}
/// <summary>
/// Maximum simultaneous long positions measured in trade units.
/// </summary>
public int MaxOpenPositions
{
get => _maxOpenPositions.Value;
set => _maxOpenPositions.Value = value;
}
/// <summary>
/// Order volume submitted with every market entry.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="IndicesTesterStrategy"/> class.
/// </summary>
public IndicesTesterStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe that drives the logic", "General");
_sessionStart = Param(nameof(SessionStart), new TimeSpan(0, 0, 0))
.SetDisplay("Session Start", "Time of day when entries become eligible", "Trading");
_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 0, 0))
.SetDisplay("Session End", "Time of day when new entries stop", "Trading");
_closeTime = Param(nameof(CloseTime), new TimeSpan(23, 30, 0))
.SetDisplay("Close Time", "Time of day used to liquidate open positions", "Risk");
_dailyTradeLimit = Param(nameof(DailyTradeLimit), 1)
.SetGreaterThanZero()
.SetDisplay("Daily Trades", "Maximum number of trades per day", "Risk");
_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Open Positions", "Maximum simultaneous long positions", "Risk");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Market order volume for new positions", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_currentDay = default;
_tradesOpenedToday = 0;
}
/// <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)
{
// Ignore unfinished candles because the original EA worked on closed data.
if (candle.State != CandleStates.Finished)
return;
var candleTime = candle.CloseTime;
if (_currentDay != candleTime.Date)
{
// Reset the intraday counters on the first candle of a new session.
_currentDay = candleTime.Date;
_tradesOpenedToday = 0;
}
var timeOfDay = candleTime.TimeOfDay;
// Liquidate open positions once the configured close time is reached.
if (Position > 0m && timeOfDay >= CloseTime)
{
SellMarket(Position);
return;
}
// Only evaluate entries strictly inside the trading window.
if (timeOfDay <= SessionStart || timeOfDay >= SessionEnd)
return;
// Respect the daily trade allowance taken from the original EA.
if (_tradesOpenedToday >= DailyTradeLimit)
return;
// Skip entries when the simultaneous position limit would be exceeded.
if (GetOpenPositionCount() >= MaxOpenPositions)
return;
var volume = TradeVolume;
if (volume <= 0m)
return;
// Submit the market order and immediately update the per-day trade counter.
BuyMarket(volume);
_tradesOpenedToday++;
}
private int GetOpenPositionCount()
{
if (Position == 0m)
return 0;
var volume = TradeVolume;
if (volume <= 0m)
return 1;
return (int)Math.Ceiling(Math.Abs(Position) / volume);
}
}
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 indices_tester_strategy(Strategy):
def __init__(self):
super(indices_tester_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._session_start = self.Param("SessionStart", TimeSpan(0, 0, 0))
self._session_end = self.Param("SessionEnd", TimeSpan(23, 0, 0))
self._close_time = self.Param("CloseTime", TimeSpan(23, 30, 0))
self._daily_trade_limit = self.Param("DailyTradeLimit", 1)
self._max_open_positions = self.Param("MaxOpenPositions", 1)
self._trade_volume = self.Param("TradeVolume", 0.1)
self._current_day = None
self._trades_opened_today = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SessionStart(self):
return self._session_start.Value
@SessionStart.setter
def SessionStart(self, value):
self._session_start.Value = value
@property
def SessionEnd(self):
return self._session_end.Value
@SessionEnd.setter
def SessionEnd(self, value):
self._session_end.Value = value
@property
def CloseTime(self):
return self._close_time.Value
@CloseTime.setter
def CloseTime(self, value):
self._close_time.Value = value
@property
def DailyTradeLimit(self):
return self._daily_trade_limit.Value
@DailyTradeLimit.setter
def DailyTradeLimit(self, value):
self._daily_trade_limit.Value = value
@property
def MaxOpenPositions(self):
return self._max_open_positions.Value
@MaxOpenPositions.setter
def MaxOpenPositions(self, value):
self._max_open_positions.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
def OnReseted(self):
super(indices_tester_strategy, self).OnReseted()
self._current_day = None
self._trades_opened_today = 0
def OnStarted2(self, time):
super(indices_tester_strategy, self).OnStarted2(time)
self._current_day = None
self._trades_opened_today = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_time = candle.CloseTime
candle_date = candle_time.Date
if self._current_day is None or self._current_day != candle_date:
self._current_day = candle_date
self._trades_opened_today = 0
time_of_day = candle_time.TimeOfDay
# Liquidate open positions once the configured close time is reached
if self.Position > 0 and time_of_day >= self.CloseTime:
self.SellMarket(self.Position)
return
# Only evaluate entries strictly inside the trading window
if time_of_day <= self.SessionStart or time_of_day >= self.SessionEnd:
return
# Respect the daily trade allowance
if self._trades_opened_today >= self.DailyTradeLimit:
return
# Skip if already have max positions
if self._get_open_position_count() >= self.MaxOpenPositions:
return
volume = self.TradeVolume
if volume <= 0:
return
# Long-only: buy
self.BuyMarket(volume)
self._trades_opened_today += 1
def _get_open_position_count(self):
if self.Position == 0:
return 0
volume = self.TradeVolume
if volume <= 0:
return 1
import math
return int(math.ceil(abs(float(self.Position)) / float(volume)))
def CreateClone(self):
return indices_tester_strategy()