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>
/// Time based opening strategy with optional trailing stop logic.
/// </summary>
public class OpenTimeStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _useCloseTime;
private readonly StrategyParam<int> _closeHour;
private readonly StrategyParam<int> _closeMinute;
private readonly StrategyParam<bool> _enableTrailing;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _tradeHour;
private readonly StrategyParam<int> _tradeMinute;
private readonly StrategyParam<int> _durationSeconds;
private readonly StrategyParam<bool> _enableSell;
private readonly StrategyParam<bool> _enableBuy;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private decimal _pipSize;
private decimal _stopOffset;
private decimal _takeOffset;
private decimal _trailOffset;
private decimal _trailStep;
private decimal? _longEntry;
private decimal? _shortEntry;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortStop;
private decimal? _shortTake;
/// <summary>
/// Initializes a new instance of the <see cref="OpenTimeStrategy"/> class.
/// </summary>
public OpenTimeStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle subscription type", "General");
_useCloseTime = Param(nameof(UseCloseTime), true)
.SetDisplay("Use Close Window", "Enable automatic closing window", "Trading");
_closeHour = Param(nameof(CloseHour), 20)
.SetDisplay("Close Hour", "Hour for the closing window", "Trading");
_closeMinute = Param(nameof(CloseMinute), 50)
.SetDisplay("Close Minute", "Minute for the closing window", "Trading");
_enableTrailing = Param(nameof(EnableTrailing), false)
.SetDisplay("Enable Trailing", "Use trailing stop logic", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 30)
.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 3)
.SetDisplay("Trailing Step", "Additional move required to shift the trail", "Risk");
_tradeHour = Param(nameof(TradeHour), 10)
.SetDisplay("Trade Hour", "Hour to start opening positions", "Trading");
_tradeMinute = Param(nameof(TradeMinute), 0)
.SetDisplay("Trade Minute", "Minute to start opening positions", "Trading");
_durationSeconds = Param(nameof(DurationSeconds), 18000)
.SetDisplay("Duration", "Window length in seconds", "Trading");
_enableSell = Param(nameof(EnableSell), true)
.SetDisplay("Enable Sell", "Allow short entries", "Trading");
_enableBuy = Param(nameof(EnableBuy), true)
.SetDisplay("Enable Buy", "Allow long entries", "Trading");
_stopLossPips = Param(nameof(StopLossPips), 500)
.SetDisplay("Stop Loss", "Initial stop loss in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
.SetDisplay("Take Profit", "Initial take profit in pips", "Risk");
}
/// <summary>
/// Candle subscription type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Indicates whether automatic closing window is enabled.
/// </summary>
public bool UseCloseTime
{
get => _useCloseTime.Value;
set => _useCloseTime.Value = value;
}
/// <summary>
/// Hour of the closing window (0-24).
/// </summary>
public int CloseHour
{
get => _closeHour.Value;
set => _closeHour.Value = value;
}
/// <summary>
/// Minute of the closing window (0-59).
/// </summary>
public int CloseMinute
{
get => _closeMinute.Value;
set => _closeMinute.Value = value;
}
/// <summary>
/// Enables trailing stop logic.
/// </summary>
public bool EnableTrailing
{
get => _enableTrailing.Value;
set => _enableTrailing.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Additional movement required to move the trailing stop.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Hour when the strategy can open positions.
/// </summary>
public int TradeHour
{
get => _tradeHour.Value;
set => _tradeHour.Value = value;
}
/// <summary>
/// Minute when the strategy can open positions.
/// </summary>
public int TradeMinute
{
get => _tradeMinute.Value;
set => _tradeMinute.Value = value;
}
/// <summary>
/// Duration of the trading window in seconds.
/// </summary>
public int DurationSeconds
{
get => _durationSeconds.Value;
set => _durationSeconds.Value = value;
}
/// <summary>
/// Enables short entries.
/// </summary>
public bool EnableSell
{
get => _enableSell.Value;
set => _enableSell.Value = value;
}
/// <summary>
/// Enables long entries.
/// </summary>
public bool EnableBuy
{
get => _enableBuy.Value;
set => _enableBuy.Value = value;
}
/// <summary>
/// Initial stop loss distance in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Initial take profit distance in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_stopOffset = 0m;
_takeOffset = 0m;
_trailOffset = 0m;
_trailStep = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Convert pip-based inputs to absolute price offsets.
_pipSize = CalculatePipSize();
_stopOffset = StopLossPips * _pipSize;
_takeOffset = TakeProfitPips * _pipSize;
_trailOffset = TrailingStopPips * _pipSize;
_trailStep = TrailingStepPips * _pipSize;
// Subscribe to candle data used for time-based processing.
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
// no protection
}
private void ProcessCandle(ICandleMessage candle)
{
// Work only with finished candles to avoid premature actions.
if (candle.State != CandleStates.Finished)
return;
var now = candle.CloseTime;
// Force-close any position during the configured closing window.
if (UseCloseTime && IsWithinWindow(now, CloseHour, CloseMinute, DurationSeconds))
{
CloseActivePositions();
return;
}
// Update trailing stops and exit if risk limits were exceeded.
UpdateTrailingStops(candle);
CheckRiskManagement(candle);
// no bound indicators to check
// Skip entries outside the trading window.
if (!IsWithinWindow(now, TradeHour, TradeMinute, DurationSeconds))
return;
// Open or reverse long positions when buying is enabled.
if (EnableBuy && Position <= 0)
{
if (Position < 0)
{
BuyMarket();
ResetShortState();
}
BuyMarket();
InitializeLongState(candle.ClosePrice);
}
else if (EnableSell && Position >= 0)
{
if (Position > 0)
{
SellMarket();
ResetLongState();
}
SellMarket();
InitializeShortState(candle.ClosePrice);
}
}
private void UpdateTrailingStops(ICandleMessage candle)
{
if (!EnableTrailing || _trailOffset <= 0m)
return;
// Move the trailing stop for long positions once the minimal step is reached.
if (Position > 0 && _longEntry.HasValue)
{
var distance = candle.ClosePrice - _longEntry.Value;
if (distance > _trailOffset + _trailStep)
{
var triggerLevel = candle.ClosePrice - (_trailOffset + _trailStep);
if (!_longStop.HasValue || _longStop.Value < triggerLevel)
_longStop = candle.ClosePrice - _trailOffset;
}
}
// Move the trailing stop for short positions in a symmetrical way.
else if (Position < 0 && _shortEntry.HasValue)
{
var distance = _shortEntry.Value - candle.ClosePrice;
if (distance > _trailOffset + _trailStep)
{
var triggerLevel = candle.ClosePrice + (_trailOffset + _trailStep);
if (!_shortStop.HasValue || _shortStop.Value > triggerLevel)
_shortStop = candle.ClosePrice + _trailOffset;
}
}
}
private void CheckRiskManagement(ICandleMessage candle)
{
if (Position > 0)
{
if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
{
SellMarket();
ResetLongState();
return;
}
if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
{
SellMarket();
ResetLongState();
}
}
else if (Position < 0)
{
if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
{
BuyMarket();
ResetShortState();
return;
}
if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
{
BuyMarket();
ResetShortState();
}
}
}
private void CloseActivePositions()
{
// Flatten the portfolio and clear cached levels.
if (Position > 0)
{
SellMarket();
ResetLongState();
}
else if (Position < 0)
{
BuyMarket();
ResetShortState();
}
}
private void InitializeLongState(decimal price)
{
// Remember entry price and derived risk levels for long trades.
_longEntry = price;
_longStop = StopLossPips > 0 ? price - _stopOffset : null;
_longTake = TakeProfitPips > 0 ? price + _takeOffset : null;
}
private void InitializeShortState(decimal price)
{
// Remember entry price and derived risk levels for short trades.
_shortEntry = price;
_shortStop = StopLossPips > 0 ? price + _stopOffset : null;
_shortTake = TakeProfitPips > 0 ? price - _takeOffset : null;
}
private void ResetLongState()
{
_longEntry = null;
_longStop = null;
_longTake = null;
}
private void ResetShortState()
{
_shortEntry = null;
_shortStop = null;
_shortTake = null;
}
private decimal CalculatePipSize()
{
// Convert MT5-style pip values into absolute price units.
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
return 1m;
var decimals = CountDecimals(step);
var multiplier = decimals == 3 || decimals == 5
? 10m
: 1m;
return step * multiplier;
}
private static int CountDecimals(decimal value)
{
// Count decimal places by repeatedly shifting the decimal point.
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
private static bool IsWithinWindow(DateTimeOffset time, int hour, int minute, int durationSeconds)
{
if (durationSeconds <= 0)
return false;
var start = BuildReferenceTime(time, hour, minute);
var end = start.AddSeconds(durationSeconds);
return time >= start && time < end;
}
private static DateTimeOffset BuildReferenceTime(DateTimeOffset reference, int hour, int minute)
{
// Align the target time with the current trading day, allowing hour values above 23.
var normalizedHour = hour;
var day = new DateTimeOffset(reference.Year, reference.Month, reference.Day, 0, 0, 0, reference.Offset);
while (normalizedHour >= 24)
{
normalizedHour -= 24;
day = day.AddDays(1);
}
if (minute < 0)
minute = 0;
else if (minute > 59)
minute = 59;
return day.AddHours(normalizedHour).AddMinutes(minute);
}
}
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
from datatype_extensions import *
class open_time_strategy(Strategy):
def __init__(self):
super(open_time_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle subscription type", "General")
self._trade_hour = self.Param("TradeHour", 10).SetDisplay("Trade Hour", "Hour to open positions", "Trading")
self._trade_minute = self.Param("TradeMinute", 0).SetDisplay("Trade Minute", "Minute to open positions", "Trading")
self._duration_seconds = self.Param("DurationSeconds", 18000).SetDisplay("Duration", "Window length in seconds", "Trading")
self._enable_buy = self.Param("EnableBuy", True).SetDisplay("Enable Buy", "Allow long entries", "Trading")
self._enable_sell = self.Param("EnableSell", True).SetDisplay("Enable Sell", "Allow short entries", "Trading")
self._sl_pips = self.Param("StopLossPips", 500).SetDisplay("Stop Loss", "Initial stop loss in pips", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 1000).SetDisplay("Take Profit", "Initial take profit in pips", "Risk")
self._use_close_time = self.Param("UseCloseTime", True).SetDisplay("Use Close Window", "Enable closing window", "Trading")
self._close_hour = self.Param("CloseHour", 20).SetDisplay("Close Hour", "Hour for closing window", "Trading")
self._close_minute = self.Param("CloseMinute", 50).SetDisplay("Close Minute", "Minute for closing window", "Trading")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(open_time_strategy, self).OnReseted()
self._long_entry = None
self._short_entry = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
def OnStarted2(self, time):
super(open_time_strategy, self).OnStarted2(time)
self._long_entry = None
self._short_entry = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
step = float(self.Security.PriceStep)
self._pip_size = step
self._stop_offset = self._sl_pips.Value * self._pip_size
self._take_offset = self._tp_pips.Value * self._pip_size
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _is_within_window(self, now, hour, minute, duration_sec):
if duration_sec <= 0:
return False
total_min = now.Hour * 60 + now.Minute
start_min = hour * 60 + minute
end_sec = (start_min * 60) + duration_sec
now_sec = now.Hour * 3600 + now.Minute * 60 + now.Second
start_sec = start_min * 60
return now_sec >= start_sec and now_sec < end_sec
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
now = candle.CloseTime
# Force close in closing window
if self._use_close_time.Value and self._is_within_window(now, self._close_hour.Value, self._close_minute.Value, self._duration_seconds.Value):
if self.Position > 0:
self.SellMarket()
self._long_entry = None
self._long_stop = None
self._long_take = None
elif self.Position < 0:
self.BuyMarket()
self._short_entry = None
self._short_stop = None
self._short_take = None
return
# Check SL/TP
if self.Position > 0:
if self._long_stop is not None and candle.LowPrice <= self._long_stop:
self.SellMarket()
self._long_entry = None
self._long_stop = None
self._long_take = None
return
if self._long_take is not None and candle.HighPrice >= self._long_take:
self.SellMarket()
self._long_entry = None
self._long_stop = None
self._long_take = None
return
elif self.Position < 0:
if self._short_stop is not None and candle.HighPrice >= self._short_stop:
self.BuyMarket()
self._short_entry = None
self._short_stop = None
self._short_take = None
return
if self._short_take is not None and candle.LowPrice <= self._short_take:
self.BuyMarket()
self._short_entry = None
self._short_stop = None
self._short_take = None
return
if not self._is_within_window(now, self._trade_hour.Value, self._trade_minute.Value, self._duration_seconds.Value):
return
if self._enable_buy.Value and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self._short_entry = None
self.BuyMarket()
self._long_entry = candle.ClosePrice
self._long_stop = candle.ClosePrice - self._stop_offset if self._sl_pips.Value > 0 else None
self._long_take = candle.ClosePrice + self._take_offset if self._tp_pips.Value > 0 else None
elif self._enable_sell.Value and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self._long_entry = None
self.SellMarket()
self._short_entry = candle.ClosePrice
self._short_stop = candle.ClosePrice + self._stop_offset if self._sl_pips.Value > 0 else None
self._short_take = candle.ClosePrice - self._take_offset if self._tp_pips.Value > 0 else None
def CreateClone(self):
return open_time_strategy()