Estratégia Power Hour Money
Esta estratégia opera durante sessões selecionadas de Nova York e abre posições quando todos os principais períodos concordam. Uma posição comprada é aberta quando as velas de mês, semana, dia e hora fecham acima de sua abertura. Uma posição vendida é aberta quando todas fecham abaixo. Trailing stops opcionais protegem os lucros e as posições podem ser fechadas às 16:45.
Detalhes
- Entrada: comprado quando todos os períodos são verdes, vendido quando todos são vermelhos.
- Filtro de sessão: NY 9:30-11:30, estendida 8:00-16:00 ou todas as sessões.
- Trailing stop: percentual para os lados comprado e vendido.
- Fim do dia: fechamento opcional de todas as posições às 16:45.
Cálculo e execução
C# e Python usam o mesmo modelo causal: após cada vela base concluída, seu fechamento é o fechamento conhecido das velas em formação de mês, semana, dia e hora. Ele é comparado estritamente com cada abertura. Cores diferentes ou igualdade mantêm a posição. Os fechamentos finais futuros dos períodos maiores nunca são consultados.
Mês, semana iniciada na segunda-feira e dia seguem o calendário de Nova York, incluindo horário de verão. As horas são identificadas em UTC para distinguir a hora repetida no outono. Depois de observar uma mudança de período, a primeira vela base disponível fornece a abertura. Um período já iniciado quando começa o histórico permanece desconhecido até seu próximo limite; o primeiro preço observado não substitui a abertura ausente. As quatro aberturas precisam ser conhecidas antes da entrada.
CandleType padrão: cinco minutos. SessionStart=09:30 e SessionEnd=11:30 usam tempo de mercado de Nova York, início incluído e fim excluído. Configure 08:00–16:00 para a sessão estendida ou IsAllSessions=true. IsAllSessions, IsTrailingEnabled e IsEndOfDayCloseEnabled são false por padrão; LongTrailingPercent=2, ShortTrailingPercent=2, EndOfDayTime=16:45.
A concordância entra com Volume quando sem posição ou reverte completamente com Volume + abs(Position). Uma ordem pendente bloqueia novas ordens; não há intervalo obrigatório. O trailing opcional começa na execução real: fecha compras quando o bid atual atinge maior bid observado * (1 - LongTrailingPercent / 100) e vendas quando o ask atinge menor ask observado * (1 + ShortTrailingPercent / 100). Preços executados também atualizam o extremo favorável. Ambos os percentuais devem estar estritamente entre 0 e 100. A reversão reinicia o extremo.
O fechamento diário opcional envia uma ordem a mercado por toda a posição no primeiro evento de tempo de mercado a partir do limite e bloqueia entradas até a meia-noite de Nova York. Cotações e um temporizador de um minuto verificam o limite independentemente das sessões e da formação das velas. Essas saídas exigem eventos de mercado e não garantem preço exato em lacunas.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Trades causal developing month, week, day and hour colors in New York sessions,
/// with optional executable-quote trailing and market-time end-of-day closing.
/// </summary>
public class PowerHourMoneyStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<TimeSpan> _sessionStart;
private readonly StrategyParam<TimeSpan> _sessionEnd;
private readonly StrategyParam<bool> _isAllSessions;
private readonly StrategyParam<bool> _isEndOfDayCloseEnabled;
private readonly StrategyParam<TimeSpan> _endOfDayTime;
private readonly StrategyParam<bool> _isTrailingEnabled;
private readonly StrategyParam<decimal> _longTrailingPercent;
private readonly StrategyParam<decimal> _shortTrailingPercent;
private DateOnly? _month;
private DateOnly? _week;
private DateOnly? _day;
private DateTime? _hour;
private decimal? _monthOpen;
private decimal? _weekOpen;
private decimal? _dayOpen;
private decimal? _hourOpen;
private Order _pendingOrder;
private Sides? _heldSide;
private decimal _extreme;
/// <summary>Initializes a 09:30–11:30 New York window, disabled optional exits and five-minute signals.</summary>
public PowerHourMoneyStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished candles supplying causal calendar opens and closes", "General");
_sessionStart = Param(nameof(SessionStart), TimeSpan.FromMinutes(570))
.SetRange(TimeSpan.Zero, TimeSpan.FromDays(1) - TimeSpan.FromTicks(1))
.SetDisplay("Session Start", "Inclusive New York wall-clock time", "Sessions");
_sessionEnd = Param(nameof(SessionEnd), TimeSpan.FromMinutes(690))
.SetRange(TimeSpan.FromTicks(1), TimeSpan.FromDays(1))
.SetDisplay("Session End", "Exclusive New York wall-clock time", "Sessions");
_isAllSessions = Param(nameof(IsAllSessions), false)
.SetDisplay("All Sessions", "Allow entries throughout the day", "Sessions");
_isEndOfDayCloseEnabled = Param(nameof(IsEndOfDayCloseEnabled), false)
.SetDisplay("End of Day Close", "Close at the cutoff and block further entries until New York midnight", "Protection");
_endOfDayTime = Param(nameof(EndOfDayTime), TimeSpan.FromMinutes(1005))
.SetRange(TimeSpan.Zero, TimeSpan.FromDays(1) - TimeSpan.FromTicks(1))
.SetDisplay("End of Day Time", "New York wall-clock cutoff", "Protection");
_isTrailingEnabled = Param(nameof(IsTrailingEnabled), false)
.SetDisplay("Trailing Enabled", "Protect both sides on real executable Level1 quotes", "Protection");
_longTrailingPercent = Param(nameof(LongTrailingPercent), 2m).SetGreaterThanZero()
.SetDisplay("Long Trailing Percent", "Percent below the highest observed bid since entry", "Protection");
_shortTrailingPercent = Param(nameof(ShortTrailingPercent), 2m).SetGreaterThanZero()
.SetDisplay("Short Trailing Percent", "Percent above the lowest observed ask since entry", "Protection");
OrderRegistering += order => _pendingOrder = order;
OwnTradeReceived += ObserveFill;
}
/// <summary>Base candle type; five minutes by default.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>Inclusive start of the New York entry window.</summary>
public TimeSpan SessionStart { get => _sessionStart.Value; set => _sessionStart.Value = value; }
/// <summary>Exclusive end of the New York entry window.</summary>
public TimeSpan SessionEnd { get => _sessionEnd.Value; set => _sessionEnd.Value = value; }
/// <summary>Allow all session times; an enabled daily cutoff still blocks late entries.</summary>
public bool IsAllSessions { get => _isAllSessions.Value; set => _isAllSessions.Value = value; }
/// <summary>Close positions and block new entries at the New York cutoff.</summary>
public bool IsEndOfDayCloseEnabled { get => _isEndOfDayCloseEnabled.Value; set => _isEndOfDayCloseEnabled.Value = value; }
/// <summary>Daily cutoff in New York wall-clock time.</summary>
public TimeSpan EndOfDayTime { get => _endOfDayTime.Value; set => _endOfDayTime.Value = value; }
/// <summary>Enable executable-quote percent trailing for both sides.</summary>
public bool IsTrailingEnabled { get => _isTrailingEnabled.Value; set => _isTrailingEnabled.Value = value; }
/// <summary>Long trailing distance in percent, strictly between zero and one hundred.</summary>
public decimal LongTrailingPercent { get => _longTrailingPercent.Value; set => _longTrailingPercent.Value = value; }
/// <summary>Short trailing distance in percent, strictly between zero and one hundred.</summary>
public decimal ShortTrailingPercent { get => _shortTrailingPercent.Value; set => _shortTrailingPercent.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
private void ResetState()
{
_month = _week = _day = null;
_hour = null;
_monthOpen = _weekOpen = _dayOpen = _hourOpen = null;
_pendingOrder = null;
_heldSide = null;
_extreme = 0m;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (SessionStart < TimeSpan.Zero || SessionStart >= SessionEnd || SessionEnd > TimeSpan.FromDays(1)
|| EndOfDayTime < TimeSpan.Zero || EndOfDayTime >= TimeSpan.FromDays(1)
|| LongTrailingPercent <= 0m || LongTrailingPercent >= 100m
|| ShortTrailingPercent <= 0m || ShortTrailingPercent >= 100m)
throw new ArgumentException("Sessions must be ordered within a day and trailing percentages within (0, 100).");
ResetState();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
if (IsEndOfDayCloseEnabled)
StartTimer(TimeSpan.FromMinutes(1), () => TryEndOfDay());
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private bool IsPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void Submit(Sides side, decimal volume, string comment)
=> RegisterOrder(new Order { Security = Security, Portfolio = Portfolio, Type = OrderTypes.Market, Side = side, Volume = volume, Comment = comment });
private bool TryEndOfDay()
{
if (!IsEndOfDayCloseEnabled || TimeZoneInfo.ConvertTimeFromUtc(CurrentTime, TimeHelper.Est).TimeOfDay < EndOfDayTime)
return false;
if (Position != 0m && !IsPending && IsFormedAndOnlineAndAllowTrading())
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), "Power Hour end-of-day close");
return true;
}
private void ObserveFill(Subscription subscription, MyTrade trade)
{
Sides? side = Position > 0m ? Sides.Buy : Position < 0m ? Sides.Sell : null;
if (side != _heldSide)
_extreme = side is null ? 0m : trade.Trade.Price;
else if (side is Sides current)
_extreme = current == Sides.Buy ? Math.Max(_extreme, trade.Trade.Price) : Math.Min(_extreme, trade.Trade.Price);
_heldSide = side;
}
private void ObserveQuote(Level1ChangeMessage quote)
{
if (TryEndOfDay() || _heldSide is not Sides side || Position == 0m)
return;
var field = side == Sides.Buy ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (quote.TryGetDecimal(field) is not decimal price || price <= 0m)
return;
_extreme = side == Sides.Buy ? Math.Max(_extreme, price) : Math.Min(_extreme, price);
var stop = side == Sides.Buy ? _extreme * (1m - LongTrailingPercent / 100m) : _extreme * (1m + ShortTrailingPercent / 100m);
if (IsTrailingEnabled && !IsPending && IsFormedAndOnlineAndAllowTrading() && (side == Sides.Buy ? price <= stop : price >= stop))
Submit(side == Sides.Buy ? Sides.Sell : Sides.Buy, Math.Abs(Position), "Power Hour trailing close");
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var local = TimeZoneInfo.ConvertTimeFromUtc(candle.OpenTime, TimeHelper.Est);
var day = DateOnly.FromDateTime(local);
var month = new DateOnly(local.Year, local.Month, 1);
var week = day.AddDays(-(((int)local.DayOfWeek + 6) % 7));
var hour = new DateTime(candle.OpenTime.Year, candle.OpenTime.Month, candle.OpenTime.Day, candle.OpenTime.Hour, 0, 0, DateTimeKind.Utc);
var midnight = local.TimeOfDay == TimeSpan.Zero;
if (_month != month)
{
_monthOpen = _month is not null || local.Day == 1 && midnight ? candle.OpenPrice : null;
_month = month;
}
if (_week != week)
{
_weekOpen = _week is not null || local.DayOfWeek == DayOfWeek.Monday && midnight ? candle.OpenPrice : null;
_week = week;
}
if (_day != day)
{
_dayOpen = _day is not null || midnight ? candle.OpenPrice : null;
_day = day;
}
if (_hour != hour)
{
_hourOpen = _hour is not null || candle.OpenTime == hour ? candle.OpenPrice : null;
_hour = hour;
}
if (TryEndOfDay() || IsPending || !IsFormedAndOnlineAndAllowTrading()
|| _monthOpen is not decimal monthly || _weekOpen is not decimal weekly
|| _dayOpen is not decimal daily || _hourOpen is not decimal hourly)
return;
var now = TimeZoneInfo.ConvertTimeFromUtc(CurrentTime, TimeHelper.Est).TimeOfDay;
if (!IsAllSessions && (now < SessionStart || now >= SessionEnd))
return;
var close = candle.ClosePrice;
if (close > monthly && close > weekly && close > daily && close > hourly && Position <= 0m)
Submit(Sides.Buy, Volume + Math.Abs(Position), "Power Hour consensus entry");
else if (close < monthly && close < weekly && close < daily && close < hourly && Position >= 0m)
Submit(Sides.Sell, Volume + Math.Abs(Position), "Power Hour consensus entry");
}
}
import clr
clr.AddReference("Ecng.Common")
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateOnly, DateTime, DateTimeKind, DayOfWeek, Decimal, TimeSpan, TimeZoneInfo, ArgumentException
from Ecng.Common import TimeHelper
from StockSharp.Messages import DataType, CandleStates, OrderStates, OrderTypes, Sides, Level1Fields, ITickTradeMessage
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class power_hour_money_strategy(Strategy):
def __init__(self):
super(power_hour_money_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying causal calendar opens and closes", "General")
self._session_start = self.Param("SessionStart", TimeSpan.FromMinutes(570)) \
.SetRange(TimeSpan.Zero, TimeSpan.FromDays(1) - TimeSpan.FromTicks(1)) \
.SetDisplay("Session Start", "Inclusive New York wall-clock time", "Sessions")
self._session_end = self.Param("SessionEnd", TimeSpan.FromMinutes(690)) \
.SetRange(TimeSpan.FromTicks(1), TimeSpan.FromDays(1)) \
.SetDisplay("Session End", "Exclusive New York wall-clock time", "Sessions")
self._is_all_sessions = self.Param("IsAllSessions", False) \
.SetDisplay("All Sessions", "Allow entries throughout the day", "Sessions")
self._is_end_of_day_close_enabled = self.Param("IsEndOfDayCloseEnabled", False) \
.SetDisplay("End of Day Close", "Close at the cutoff and block further entries until New York midnight", "Protection")
self._end_of_day_time = self.Param("EndOfDayTime", TimeSpan.FromMinutes(1005)) \
.SetRange(TimeSpan.Zero, TimeSpan.FromDays(1) - TimeSpan.FromTicks(1)) \
.SetDisplay("End of Day Time", "New York wall-clock cutoff", "Protection")
self._is_trailing_enabled = self.Param("IsTrailingEnabled", False) \
.SetDisplay("Trailing Enabled", "Protect both sides on real executable Level1 quotes", "Protection")
self._long_trailing_percent = self.Param("LongTrailingPercent", Decimal.Parse("2")).SetGreaterThanZero() \
.SetDisplay("Long Trailing Percent", "Percent below the highest observed bid since entry", "Protection")
self._short_trailing_percent = self.Param("ShortTrailingPercent", Decimal.Parse("2")).SetGreaterThanZero() \
.SetDisplay("Short Trailing Percent", "Percent above the lowest observed ask since entry", "Protection")
self._reset_state()
self.OrderRegistering += self._observe_order
self.OwnTradeReceived += self._observe_fill
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._month = self._week = self._day = self._hour = None
self._month_open = self._week_open = self._day_open = self._hour_open = None
self._pending_order = None
self._held_side = None
self._extreme = Decimal.Zero
def _observe_order(self, order):
self._pending_order = order
def OnReseted(self):
super(power_hour_money_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(power_hour_money_strategy, self).OnStarted2(time)
if self._session_start.Value < TimeSpan.Zero or self._session_start.Value >= self._session_end.Value \
or self._session_end.Value > TimeSpan.FromDays(1) or self._end_of_day_time.Value < TimeSpan.Zero \
or self._end_of_day_time.Value >= TimeSpan.FromDays(1) \
or self._long_trailing_percent.Value <= 0 or self._long_trailing_percent.Value >= 100 \
or self._short_trailing_percent.Value <= 0 or self._short_trailing_percent.Value >= 100:
raise ArgumentException("Sessions must be ordered within a day and trailing percentages within (0, 100).")
self._reset_state()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
if self._is_end_of_day_close_enabled.Value:
self.StartTimer(TimeSpan.FromMinutes(1), self._try_end_of_day)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _is_pending(self):
order = self._pending_order
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = comment
self.RegisterOrder(order)
def _try_end_of_day(self):
if not self._is_end_of_day_close_enabled.Value \
or TimeZoneInfo.ConvertTimeFromUtc(self.CurrentTime, TimeHelper.Est).TimeOfDay < self._end_of_day_time.Value:
return False
if self.Position != 0 and not self._is_pending() and self.IsFormedAndOnlineAndAllowTrading():
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), "Power Hour end-of-day close")
return True
def _observe_fill(self, subscription, trade):
# Reflection reads the explicit CLR interface member for every tick-message implementation.
price = clr.GetClrType(ITickTradeMessage).GetProperty("Price").GetValue(trade.Trade)
side = Sides.Buy if self.Position > 0 else Sides.Sell if self.Position < 0 else None
if side != self._held_side:
self._extreme = Decimal.Zero if side is None else price
elif side is not None:
self._extreme = max(self._extreme, price) if side == Sides.Buy else min(self._extreme, price)
self._held_side = side
def _observe_quote(self, quote):
if self._try_end_of_day() or self._held_side is None or self.Position == 0:
return
side = self._held_side
field = Level1Fields.BestBidPrice if side == Sides.Buy else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(field):
return
price = quote.Changes[field]
if price <= 0:
return
self._extreme = max(self._extreme, price) if side == Sides.Buy else min(self._extreme, price)
percent = self._long_trailing_percent.Value if side == Sides.Buy else self._short_trailing_percent.Value
stop = self._extreme * (Decimal(1) - percent / Decimal(100)) if side == Sides.Buy \
else self._extreme * (Decimal(1) + percent / Decimal(100))
triggered = price <= stop if side == Sides.Buy else price >= stop
if self._is_trailing_enabled.Value and not self._is_pending() and self.IsFormedAndOnlineAndAllowTrading() and triggered:
self._submit(Sides.Sell if side == Sides.Buy else Sides.Buy, abs(self.Position), "Power Hour trailing close")
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
local = TimeZoneInfo.ConvertTimeFromUtc(candle.OpenTime, TimeHelper.Est)
day = DateOnly.FromDateTime(local)
month = DateOnly(local.Year, local.Month, 1)
week = day.AddDays(-((int(local.DayOfWeek) + 6) % 7))
hour = DateTime(candle.OpenTime.Year, candle.OpenTime.Month, candle.OpenTime.Day, candle.OpenTime.Hour, 0, 0, DateTimeKind.Utc)
midnight = local.TimeOfDay == TimeSpan.Zero
if self._month != month:
self._month_open = candle.OpenPrice if self._month is not None or local.Day == 1 and midnight else None
self._month = month
if self._week != week:
self._week_open = candle.OpenPrice if self._week is not None or local.DayOfWeek == DayOfWeek.Monday and midnight else None
self._week = week
if self._day != day:
self._day_open = candle.OpenPrice if self._day is not None or midnight else None
self._day = day
if self._hour != hour:
self._hour_open = candle.OpenPrice if self._hour is not None or candle.OpenTime == hour else None
self._hour = hour
if self._try_end_of_day() or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading() \
or any(value is None for value in (self._month_open, self._week_open, self._day_open, self._hour_open)):
return
now = TimeZoneInfo.ConvertTimeFromUtc(self.CurrentTime, TimeHelper.Est).TimeOfDay
if not self._is_all_sessions.Value and (now < self._session_start.Value or now >= self._session_end.Value):
return
close = candle.ClosePrice
opens = (self._month_open, self._week_open, self._day_open, self._hour_open)
if all(close > value for value in opens) and self.Position <= 0:
self._submit(Sides.Buy, self.Volume + abs(self.Position), "Power Hour consensus entry")
elif all(close < value for value in opens) and self.Position >= 0:
self._submit(Sides.Sell, self.Volume + abs(self.Position), "Power Hour consensus entry")
def CreateClone(self):
return power_hour_money_strategy()