経済カレンダー戦略のサンプル検出
概要
サンプル検出経済カレンダー戦略は、元の MetaTrader エキスパート アドバイザー SampleDetectEconomicCalendar.mq5 の動作を再現します。この戦略は、手動で提供された経済カレンダー イベントのリストを監視し、設定された通貨に大きな影響を与えるイベントが近づいている場合に、現在の買値/売値を中心に対称的な逆指値ペアを配置します。プロテクティブストップ、オプションのテイクプロフィットレベル、トレーリングエグジットは、ソースコードの資金管理ロジックを複製します。
MQL バージョンとは異なり、StockSharp ポートは MetaTrader カレンダー サービスにアクセスできません。代わりに、イベントはユーザーによって CalendarDefinition パラメータを通じて提供されます。
仕組み
- この戦略はレベル 1 データをサブスクライブして買値/売値を追跡します。
CalendarDefinitionで定義されたカレンダー行は起動時に解析されます。BaseCurrencyに一致する重要度の高いイベントごとに、戦略は次のようになります。- リリース前に
LeadMinutesまで待機します。 - 注文量 (固定またはリスクベース) を計算します。
- 現在の価格から
BuyDistancePointsとSellDistancePointsで買い/売りのストップ注文を出します。
- リリース前に
- リリース後、保留中の注文は、
PostMinutesが経過するか、合計タイムアウトがExpiryMinutesに達するとキャンセルされます。 - 一方がトリガーされると、反対側の注文はキャンセルされます。オープンポジションは、ストップロス、オプションのテイクプロフィット、およびポイントで表されるトレーリングストップ距離で管理されます。
パラメーター
| パラメータ | 説明 |
|---|---|
TradeNews |
スケジュールされたニュースイベントの前後に未決注文を発注できるようにします。 |
OrderVolume |
資金管理が無効な場合に使用される固定注文量。 |
StopLossPoints |
計器ポイントでのストップロス距離。無効にするには 0 に設定します。 |
TakeProfitPoints |
テイクプロフィット距離(ポイント単位)。無効にするには 0 に設定します。 |
TrailingStopPoints |
トレーリングストップの距離 (ポイント単位)。末尾を無効にするには 0 に設定します。 |
ExpiryMinutes |
リリース後の未決注文の最大存続期間。 |
UseMoneyManagement |
有効にすると、残高リスクからボリュームが計算されます。 |
RiskPercent |
取引ごとにリスクがかかるポートフォリオ資本の割合(資金管理がアクティブな場合にのみ使用されます)。 |
BuyDistancePoints |
買いストップエントリーのアスクの上のオフセット。 |
SellDistancePoints |
売りストップエントリーの入札値を下回るオフセット。 |
LeadMinutes |
未決注文が送信されるリリースの数分前。 |
PostMinutes |
リリース後、無人注文がキャンセルされるまでの数分。 |
BaseCurrency |
カレンダーエントリに表示する必要がある通貨コード (デフォルトは USD)。 |
CalendarDefinition |
カレンダーイベントを含む複数行の文字列。 |
カレンダー定義形式
次の形式で 1 行に 1 つのイベントを入力します。
「」 yyyy-MM-dd HH:mm;CUR;高;イベントのタイトル 「」
yyyy-MM-dd HH:mm— UTC のタイムスタンプ。秒数はオプションです。複数の日付形式 (yyyy/MM/dd、dd.MM.yyyy) もサポートされています。CUR— 通貨コード (例:USD)。BaseCurrencyに一致するイベントのみが取引されます。High- 重要キーワード (High、Medium、Low、またはNfp)。Highのみが取引をトリガーします。Event title— ログ用の自由テキスト。
例:
「」 2024-06-12 18:00;USD;高値;FOMC声明 2024-07-05 12:30;USD;Nfp;非農業部門雇用者数 「」
リスク管理
UseMoneyManagementが オフの場合、注文はOrderVolumeパラメータを使用して行われます。UseMoneyManagementが オン の場合、戦略は構成されたStopLossPointsを使用してポートフォリオ値のRiskPercentをリスクにさらします。 Exchange のボリューム制限 (最小/最大ステップ) が尊重されます。- トレーリング ロジックはオリジナルの EA を反映しています。ストップロスとテイクプロフィットのエグジットが強制され、価格が
TrailingStopPointsまでに有利に推移すると、トレーリング ストップが取引を保護します。
MQL エキスパートアドバイザーとの違い
- 経済カレンダー イベントは、
CalendarDefinitionで手動で指定する必要があります。 - 戦略インスタンスごとに 1 つの金融商品/通貨ペアのみが処理されます。
- StockSharp のストップ注文は MetaTrader スタイルの
ORDER_TIME_SPECIFIEDフラグを公開しないため、未決注文の有効期限はPostMinutes/ExpiryMinutesタイマーで内部的に処理されます。
使用上の注意
- 戦略を開始する前に、
CalendarDefinition行を構成します。 TradeNewsを有効にし、必要なリスク パラメータを設定します。- ニュースウィンドウの前に買値/売値の更新が届くように、レベル 1 データが利用可能であることを確認します。
- ログを確認して、各イベントの前後で予想どおりに注文とキャンセルが行われていることを確認します。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;
/// <summary>
/// Economic-calendar breakout strategy driven by manually supplied events and Level1 quotes.
/// </summary>
public class SampleDetectEconomicCalendarStrategy : Strategy
{
private readonly StrategyParam<bool> _tradeNews;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _trailingStopPoints;
private readonly StrategyParam<int> _expiryMinutes;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _buyDistancePoints;
private readonly StrategyParam<int> _sellDistancePoints;
private readonly StrategyParam<int> _leadMinutes;
private readonly StrategyParam<int> _postMinutes;
private readonly StrategyParam<string> _baseCurrency;
private readonly StrategyParam<string> _calendarDefinition;
private readonly List<CalendarEvent> _events = [];
private decimal? _bestBid;
private decimal? _bestAsk;
private CalendarEvent _armedEvent;
private Order _buyStopOrder;
private Order _sellStopOrder;
private Order _winner;
private Order _exitOrder;
private bool _cancelPair;
private readonly HashSet<long> _cancelRequested = [];
private ITimerHandler _pendingTimer;
private decimal _filledPosition;
private decimal _entryValue;
private decimal? _trailingPrice;
public bool TradeNews { get => _tradeNews.Value; set => _tradeNews.Value = value; }
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public int TrailingStopPoints { get => _trailingStopPoints.Value; set => _trailingStopPoints.Value = value; }
public int ExpiryMinutes { get => _expiryMinutes.Value; set => _expiryMinutes.Value = value; }
public bool UseMoneyManagement { get => _useMoneyManagement.Value; set => _useMoneyManagement.Value = value; }
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
public int BuyDistancePoints { get => _buyDistancePoints.Value; set => _buyDistancePoints.Value = value; }
public int SellDistancePoints { get => _sellDistancePoints.Value; set => _sellDistancePoints.Value = value; }
public int LeadMinutes { get => _leadMinutes.Value; set => _leadMinutes.Value = value; }
public int PostMinutes { get => _postMinutes.Value; set => _postMinutes.Value = value; }
public string BaseCurrency { get => _baseCurrency.Value; set => _baseCurrency.Value = value; }
public string CalendarDefinition { get => _calendarDefinition.Value; set => _calendarDefinition.Value = value; }
public SampleDetectEconomicCalendarStrategy()
{
_tradeNews = Param(nameof(TradeNews), true);
_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
_stopLossPoints = Param(nameof(StopLossPoints), 100).SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 200).SetNotNegative();
_trailingStopPoints = Param(nameof(TrailingStopPoints), 50).SetNotNegative();
_expiryMinutes = Param(nameof(ExpiryMinutes), 120).SetNotNegative();
_useMoneyManagement = Param(nameof(UseMoneyManagement), false);
_riskPercent = Param(nameof(RiskPercent), 1m).SetGreaterThanZero();
_buyDistancePoints = Param(nameof(BuyDistancePoints), 10).SetGreaterThanZero();
_sellDistancePoints = Param(nameof(SellDistancePoints), 10).SetGreaterThanZero();
_leadMinutes = Param(nameof(LeadMinutes), 15).SetNotNegative();
_postMinutes = Param(nameof(PostMinutes), 30).SetNotNegative();
_baseCurrency = Param(nameof(BaseCurrency), "USD");
_calendarDefinition = Param(nameof(CalendarDefinition), string.Empty);
Trades.TradeAdded += ProcessTrade;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, DataType.Level1), (Security, DataType.Ticks)];
protected override void OnReseted()
{
base.OnReseted();
_events.Clear();
_bestBid = null;
_bestAsk = null;
_pendingTimer?.Dispose();
_pendingTimer = null;
_armedEvent = null;
_buyStopOrder = _sellStopOrder = _winner = _exitOrder = null;
_cancelPair = false;
_cancelRequested.Clear();
_filledPosition = _entryValue = 0m;
_trailingPrice = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security?.PriceStep is not decimal point || point <= 0m)
throw new InvalidOperationException("A positive security price step is required for news stops.");
ParseCalendar();
// Native stop matching needs real trades, not synthetic OHLC prints or bid/ask-only messages.
Subscribe(new Subscription(DataType.Ticks, Security));
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
}
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
_bestBid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
_bestAsk = ask;
var now = message.ServerTime;
ProcessPending(now);
if (_bestBid is null || _bestAsk is null)
return;
if (Position != 0m)
{
ManageOpenPosition();
return;
}
if (TradeNews && _armedEvent is null && _exitOrder is not { State: OrderStates.Active or OrderStates.Pending }
&& IsFormedAndOnlineAndAllowTrading())
TryArm(now);
}
private void TryArm(DateTime now)
{
var evt = _events
.Where(e => !e.Consumed &&
e.Currency.EqualsIgnoreCase(BaseCurrency) &&
e.HighImpact &&
now >= e.Time.AddMinutes(-LeadMinutes) &&
now < e.Time.AddMinutes(PostMinutes) &&
(ExpiryMinutes == 0 || now < e.Time.AddMinutes(ExpiryMinutes)))
.OrderBy(e => e.Time)
.FirstOrDefault();
if (evt is null)
return;
var step = GetPoint();
var volume = CalculateVolume();
if (volume <= 0m)
throw new InvalidOperationException("The configured volume limits admit no positive news order volume.");
_armedEvent = evt;
evt.Consumed = true;
_cancelPair = false;
_winner = null;
_cancelRequested.Clear();
_buyStopOrder = CreateStop(Sides.Buy, _bestAsk.Value + BuyDistancePoints * step, volume);
_sellStopOrder = CreateStop(Sides.Sell, _bestBid.Value - SellDistancePoints * step, volume);
_pendingTimer = StartTimer(TimeSpan.FromSeconds(1), () => ProcessPending(CurrentTime));
RegisterOrder(_buyStopOrder);
RegisterOrder(_sellStopOrder);
}
private Order CreateStop(Sides side, decimal activation, decimal volume)
=> new()
{
Security = Security,
Portfolio = Portfolio,
Side = side,
Volume = volume,
Type = OrderTypes.Conditional,
Condition = new StopOrderCondition { ActivationPrice = activation },
Comment = "Calendar " + _armedEvent.Title,
};
private bool IsExpired(DateTime now)
=> now >= _armedEvent.Time.AddMinutes(PostMinutes)
|| (ExpiryMinutes > 0 && now >= _armedEvent.Time.AddMinutes(ExpiryMinutes));
private void ProcessPending(DateTime now)
{
if (_armedEvent is null)
return;
_cancelPair |= !TradeNews || IsExpired(now)
|| _buyStopOrder.State == OrderStates.Failed || _sellStopOrder.State == OrderStates.Failed;
if (_cancelPair || (_winner is not null && _winner != _buyStopOrder)) RequestCancel(_buyStopOrder);
if (_cancelPair || (_winner is not null && _winner != _sellStopOrder)) RequestCancel(_sellStopOrder);
// Keep both references until cancellation/execution is acknowledged, including delayed registrations.
if (IsTerminal(_buyStopOrder) && IsTerminal(_sellStopOrder))
{
_armedEvent = null;
_buyStopOrder = _sellStopOrder = _winner = null;
_pendingTimer?.Dispose();
_pendingTimer = null;
}
}
private static bool IsTerminal(Order order) => order.State is OrderStates.Done or OrderStates.Failed;
protected override void OnOrderRegistered(Order order)
{
base.OnOrderRegistered(order);
if (_armedEvent is not null && (order == _buyStopOrder || order == _sellStopOrder))
ProcessPending(CurrentTime);
}
private void RequestCancel(Order order)
{
if (order.State == OrderStates.Active && _cancelRequested.Add(order.TransactionId))
CancelOrder(order);
}
private void ProcessTrade(MyTrade trade)
{
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
var signed = trade.Order.Side == Sides.Buy ? volume : -volume;
var previous = _filledPosition;
if (previous == 0m || Math.Sign(previous) == Math.Sign(signed))
{
_entryValue += price * volume;
_filledPosition += signed;
}
else
{
_entryValue -= _entryValue / Math.Abs(previous) * Math.Min(volume, Math.Abs(previous));
_filledPosition += signed;
if (_filledPosition != 0m && Math.Sign(_filledPosition) != Math.Sign(previous))
_entryValue = price * Math.Abs(_filledPosition);
}
if (_filledPosition == 0m) _entryValue = 0m;
if (Math.Sign(previous) != Math.Sign(_filledPosition)) _trailingPrice = null;
if (trade.Order == _buyStopOrder || trade.Order == _sellStopOrder)
{
_winner ??= trade.Order;
RequestCancel(trade.Order == _buyStopOrder ? _sellStopOrder : _buyStopOrder);
}
}
private void ManageOpenPosition()
{
if (_exitOrder is { State: OrderStates.Pending or OrderStates.Active } || _filledPosition == 0m)
return;
var longPosition = Position > 0m;
var direction = longPosition ? 1m : -1m;
var price = longPosition ? _bestBid.Value : _bestAsk.Value;
var entry = _entryValue / Math.Abs(_filledPosition);
var point = GetPoint();
decimal? stop = StopLossPoints > 0 ? entry - direction * StopLossPoints * point : null;
if (TrailingStopPoints > 0 && direction * (price - entry) >= TrailingStopPoints * point)
{
var candidate = price - direction * TrailingStopPoints * point;
if (_trailingPrice is null || direction * (candidate - _trailingPrice.Value) > 0m)
_trailingPrice = candidate;
}
if (_trailingPrice is decimal trailing && (stop is null || direction * (trailing - stop.Value) > 0m))
stop = trailing;
var stopHit = stop is decimal level && direction * (price - level) <= 0m;
var takeHit = TakeProfitPoints > 0 && direction * (price - entry) >= TakeProfitPoints * point;
if (stopHit || takeHit)
{
_cancelPair = true;
ProcessPending(CurrentTime);
_exitOrder = longPosition ? SellMarket(Math.Abs(Position)) : BuyMarket(Math.Abs(Position));
}
}
private decimal CalculateVolume()
{
if (!UseMoneyManagement || StopLossPoints <= 0)
return NormalizeVolume(OrderVolume);
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance <= 0m)
return NormalizeVolume(OrderVolume);
var riskMoney = balance * RiskPercent / 100m;
var stepPrice = Security?.StepPrice ?? 0m;
var point = GetPoint();
var lossPerUnit = stepPrice > 0m
? StopLossPoints * stepPrice
: StopLossPoints * point * (Security?.Multiplier ?? 1m);
if (lossPerUnit <= 0m)
return NormalizeVolume(OrderVolume);
return NormalizeVolume(riskMoney / lossPerUnit);
}
private decimal NormalizeVolume(decimal volume)
{
var minimum = Math.Max(0m, Security.MinVolume ?? 0m);
var maximum = Security.MaxVolume is decimal max && max > 0m ? max : decimal.MaxValue;
if (Security?.VolumeStep is decimal step && step > 0m)
{
minimum = Math.Ceiling(minimum / step) * step;
if (maximum != decimal.MaxValue) maximum = Math.Floor(maximum / step) * step;
volume = Math.Min(volume, maximum);
volume = Math.Floor(volume / step) * step;
}
if (minimum > maximum)
throw new InvalidOperationException("The security volume grid has no quantity within its min/max limits.");
return Math.Max(minimum, Math.Min(volume, maximum));
}
private decimal GetPoint()
=> Security.PriceStep.Value;
private void ParseCalendar()
{
_events.Clear();
var formats = new[] { "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss", "dd.MM.yyyy HH:mm", "dd.MM.yyyy HH:mm:ss" };
foreach (var raw in (CalendarDefinition ?? string.Empty).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var parts = raw.Split(';');
if (parts.Length < 4)
continue;
if (!DateTime.TryParseExact(parts[0].Trim(), formats, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var when))
continue;
var importance = parts[2].Trim();
_events.Add(new CalendarEvent
{
Time = when,
Currency = parts[1].Trim(),
HighImpact = importance.EqualsIgnoreCase("High"),
Title = string.Join(";", parts.Skip(3)).Trim(),
});
}
}
private sealed class CalendarEvent
{
public DateTime Time { get; init; }
public string Currency { get; init; }
public bool HighImpact { get; init; }
public string Title { get; init; }
public bool Consumed { get; set; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.MatchingEngine")
from System import DateTime, TimeSpan, Math, Decimal
from System.Globalization import CultureInfo, DateTimeStyles
from StockSharp.Messages import DataType, Level1Fields, Sides, OrderTypes, OrderStates, ITickTradeMessage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.MatchingEngine import StopOrderCondition
_tick_price = clr.GetClrType(ITickTradeMessage).GetProperty("Price")
_tick_volume = clr.GetClrType(ITickTradeMessage).GetProperty("Volume")
class sample_detect_economic_calendar_strategy(Strategy):
def __init__(self):
super(sample_detect_economic_calendar_strategy, self).__init__()
self._trade_news = self.Param("TradeNews", True)
self._order_volume = self.Param("OrderVolume", 0.1).SetGreaterThanZero()
self._stop_loss = self.Param("StopLossPoints", 100).SetNotNegative()
self._take_profit = self.Param("TakeProfitPoints", 200).SetNotNegative()
self._trailing_stop = self.Param("TrailingStopPoints", 50).SetNotNegative()
self._expiry_minutes = self.Param("ExpiryMinutes", 120).SetNotNegative()
self._use_money_management = self.Param("UseMoneyManagement", False)
self._risk_percent = self.Param("RiskPercent", 1.0).SetGreaterThanZero()
self._buy_distance = self.Param("BuyDistancePoints", 10).SetGreaterThanZero()
self._sell_distance = self.Param("SellDistancePoints", 10).SetGreaterThanZero()
self._lead_minutes = self.Param("LeadMinutes", 15).SetNotNegative()
self._post_minutes = self.Param("PostMinutes", 30).SetNotNegative()
self._base_currency = self.Param("BaseCurrency", "USD")
self._calendar_definition = self.Param("CalendarDefinition", "")
self._pending_timer = None
self._reset_state()
self.Trades.TradeAdded += self._process_trade
def GetWorkingSecurities(self):
return [(self.Security, DataType.Level1), (self.Security, DataType.Ticks)]
def OnReseted(self):
super(sample_detect_economic_calendar_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
if self._pending_timer is not None:
self._pending_timer.Dispose()
self._pending_timer = None
self._events = []
self._best_bid = None
self._best_ask = None
self._armed_event = None
self._buy_stop_order = None
self._sell_stop_order = None
self._winner = None
self._exit_order = None
self._cancel_pair = False
self._cancel_requested = set()
self._filled_position = Decimal.Zero
self._entry_value = Decimal.Zero
self._trailing_price = None
def OnStarted2(self, time):
super(sample_detect_economic_calendar_strategy, self).OnStarted2(time)
if self.Security.PriceStep is None or self.Security.PriceStep <= 0:
raise ValueError("A positive security price step is required for news stops.")
self._parse_calendar()
# Real trades drive native stop matching; bid/ask updates need both one-sided bindings.
self.Subscribe(Subscription(DataType.Ticks, self.Security))
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._process_level1).Start()
def _process_level1(self, message):
bid = message.Changes[Level1Fields.BestBidPrice] if message.Changes.ContainsKey(Level1Fields.BestBidPrice) else None
ask = message.Changes[Level1Fields.BestAskPrice] if message.Changes.ContainsKey(Level1Fields.BestAskPrice) else None
if bid is not None and bid > 0:
self._best_bid = bid
if ask is not None and ask > 0:
self._best_ask = ask
now = message.ServerTime
self._process_pending(now)
if self._best_bid is None or self._best_ask is None:
return
if self.Position != 0:
self._manage_open_position()
return
exit_pending = self._exit_order is not None and self._exit_order.State in (OrderStates.Active, OrderStates.Pending)
if (bool(self._trade_news.Value) and self._armed_event is None and not exit_pending
and self.IsFormedAndOnlineAndAllowTrading()):
self._try_arm(now)
def _try_arm(self, now):
currency = str(self._base_currency.Value)
lead = int(self._lead_minutes.Value)
post = int(self._post_minutes.Value)
expiry = int(self._expiry_minutes.Value)
candidates = [
e for e in self._events
if not e["consumed"] and e["high"] and e["currency"].lower() == currency.lower()
and now >= e["time"].AddMinutes(-lead) and now < e["time"].AddMinutes(post)
and (expiry == 0 or now < e["time"].AddMinutes(expiry))
]
if not candidates:
return
event = min(candidates, key=lambda e: e["time"])
point = self._point()
volume = self._calculate_volume()
if volume <= 0:
raise ValueError("The configured volume limits admit no positive news order volume.")
self._armed_event = event
event["consumed"] = True
self._cancel_pair = False
self._winner = None
self._cancel_requested.clear()
self._buy_stop_order = self._create_stop(Sides.Buy, self._best_ask + Decimal(self._buy_distance.Value) * point, volume)
self._sell_stop_order = self._create_stop(Sides.Sell, self._best_bid - Decimal(self._sell_distance.Value) * point, volume)
self._pending_timer = self.StartTimer(TimeSpan.FromSeconds(1), self._timer_tick)
self.RegisterOrder(self._buy_stop_order)
self.RegisterOrder(self._sell_stop_order)
def _create_stop(self, side, activation, volume):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Side = side
order.Volume = volume
order.Type = OrderTypes.Conditional
condition = StopOrderCondition()
condition.ActivationPrice = activation
order.Condition = condition
order.Comment = "Calendar " + self._armed_event["title"]
return order
def _timer_tick(self):
self._process_pending(self.CurrentTime)
def _is_expired(self, now):
post_expired = now >= self._armed_event["time"].AddMinutes(int(self._post_minutes.Value))
expiry = int(self._expiry_minutes.Value)
life_expired = expiry > 0 and now >= self._armed_event["time"].AddMinutes(expiry)
return post_expired or life_expired
def _process_pending(self, now):
if self._armed_event is None:
return
self._cancel_pair = (self._cancel_pair or not bool(self._trade_news.Value) or self._is_expired(now)
or self._buy_stop_order.State == OrderStates.Failed
or self._sell_stop_order.State == OrderStates.Failed)
if self._cancel_pair or (self._winner is not None and self._winner != self._buy_stop_order):
self._request_cancel(self._buy_stop_order)
if self._cancel_pair or (self._winner is not None and self._winner != self._sell_stop_order):
self._request_cancel(self._sell_stop_order)
# A cancel request is not a terminal acknowledgement. Keep delayed registrations tracked.
terminal = (OrderStates.Done, OrderStates.Failed)
if self._buy_stop_order.State in terminal and self._sell_stop_order.State in terminal:
self._armed_event = None
self._buy_stop_order = self._sell_stop_order = self._winner = None
if self._pending_timer is not None:
self._pending_timer.Dispose()
self._pending_timer = None
def _request_cancel(self, order):
if order.State == OrderStates.Active and order.TransactionId not in self._cancel_requested:
self._cancel_requested.add(order.TransactionId)
self.CancelOrder(order)
def OnOrderRegistered(self, order):
super(sample_detect_economic_calendar_strategy, self).OnOrderRegistered(order)
if self._armed_event is not None and (order == self._buy_stop_order or order == self._sell_stop_order):
self._process_pending(self.CurrentTime)
def _process_trade(self, trade):
price = _tick_price.GetValue(trade.Trade)
volume = _tick_volume.GetValue(trade.Trade)
signed = volume if trade.Order.Side == Sides.Buy else -volume
previous = self._filled_position
if previous == 0 or Math.Sign(previous) == Math.Sign(signed):
self._entry_value += price * volume
self._filled_position += signed
else:
self._entry_value -= self._entry_value / Math.Abs(previous) * Math.Min(volume, Math.Abs(previous))
self._filled_position += signed
if self._filled_position != 0 and Math.Sign(self._filled_position) != Math.Sign(previous):
self._entry_value = price * Math.Abs(self._filled_position)
if self._filled_position == 0:
self._entry_value = Decimal.Zero
if Math.Sign(previous) != Math.Sign(self._filled_position):
self._trailing_price = None
if trade.Order == self._buy_stop_order or trade.Order == self._sell_stop_order:
if self._winner is None:
self._winner = trade.Order
self._request_cancel(self._sell_stop_order if trade.Order == self._buy_stop_order else self._buy_stop_order)
def _manage_open_position(self):
if (self._exit_order is not None and self._exit_order.State in (OrderStates.Pending, OrderStates.Active)) or self._filled_position == 0:
return
long_position = self.Position > 0
direction = Decimal(1) if long_position else Decimal(-1)
price = self._best_bid if long_position else self._best_ask
entry = self._entry_value / Math.Abs(self._filled_position)
point = self._point()
trail = int(self._trailing_stop.Value)
sl = int(self._stop_loss.Value)
tp = int(self._take_profit.Value)
stop = entry - direction * Decimal(sl) * point if sl > 0 else None
if trail > 0 and direction * (price - entry) >= Decimal(trail) * point:
candidate = price - direction * Decimal(trail) * point
if self._trailing_price is None or direction * (candidate - self._trailing_price) > 0:
self._trailing_price = candidate
if self._trailing_price is not None and (stop is None or direction * (self._trailing_price - stop) > 0):
stop = self._trailing_price
stop_hit = stop is not None and direction * (price - stop) <= 0
take_hit = tp > 0 and direction * (price - entry) >= Decimal(tp) * point
if stop_hit or take_hit:
self._cancel_pair = True
self._process_pending(self.CurrentTime)
self._exit_order = self.SellMarket(Math.Abs(self.Position)) if long_position else self.BuyMarket(Math.Abs(self.Position))
def _calculate_volume(self):
if not bool(self._use_money_management.Value) or int(self._stop_loss.Value) <= 0:
return self._normalize_volume(Decimal(self._order_volume.Value))
balance = Decimal.Zero
if self.Portfolio is not None:
value = self.Portfolio.CurrentValue if self.Portfolio.CurrentValue is not None else self.Portfolio.BeginValue
balance = value if value is not None else Decimal.Zero
if balance <= 0:
return self._normalize_volume(Decimal(self._order_volume.Value))
risk_money = balance * Decimal(self._risk_percent.Value) / Decimal(100)
step_price = self.Security.StepPrice if self.Security.StepPrice is not None else Decimal.Zero
point = self._point()
multiplier = self.Security.Multiplier if self.Security.Multiplier is not None else Decimal(1)
loss_per_unit = Decimal(self._stop_loss.Value) * (step_price if step_price > 0 else point * multiplier)
if loss_per_unit <= 0:
return self._normalize_volume(Decimal(self._order_volume.Value))
return self._normalize_volume(risk_money / loss_per_unit)
def _normalize_volume(self, volume):
minimum = Math.Max(Decimal.Zero, self.Security.MinVolume if self.Security.MinVolume is not None else Decimal.Zero)
maximum = self.Security.MaxVolume if self.Security.MaxVolume is not None and self.Security.MaxVolume > 0 else Decimal.MaxValue
if self.Security.VolumeStep is not None and self.Security.VolumeStep > 0:
step = self.Security.VolumeStep
minimum = Math.Ceiling(minimum / step) * step
if maximum != Decimal.MaxValue:
maximum = Math.Floor(maximum / step) * step
volume = Math.Floor(Math.Min(volume, maximum) / step) * step
if minimum > maximum:
raise ValueError("The security volume grid has no quantity within its min/max limits.")
return Math.Max(minimum, Math.Min(volume, maximum))
def _point(self):
return self.Security.PriceStep
def _parse_calendar(self):
self._events = []
formats = ["yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss", "dd.MM.yyyy HH:mm", "dd.MM.yyyy HH:mm:ss"]
for raw in str(self._calendar_definition.Value or "").replace("\r", "").split("\n"):
if not raw.strip():
continue
parts = raw.split(";")
if len(parts) < 4:
continue
parsed = DateTime()
ok = False
for fmt in formats:
try:
parsed = DateTime.ParseExact(
parts[0].strip(), fmt, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)
ok = True
break
except Exception:
pass
if not ok:
continue
importance = parts[2].strip().lower()
self._events.append({
"time": parsed,
"currency": parts[1].strip(),
"high": importance == "high",
"title": ";".join(parts[3:]).strip(),
"consumed": False,
})
def CreateClone(self):
return sample_detect_economic_calendar_strategy()