Exemplo de impulso de candles apenas comprado, com uma entrada por data de Nova York. “Gap” significa aumento do fechamento sobre o fechamento anterior; não exige gap noturno de abertura ou cruzamento EMA.
Entrada e tamanho
Somente candles concluídos, de cinco minutos por padrão. O primeiro registra fechamento e volume anteriores. A partir de posição zero exige candle estritamente de alta, intervalo high-low positivo, volume total > MinVolume, fechamento anterior positivo e 100 * (close - previousClose) / previousClose >= MinGainPct.
bodyPercent = 100 * (close - open) / (high - low): ≥90% solicita 100% de Volume; ≥85% e <90% solicita 50%; ≥75% e <85% solicita 25%; abaixo de 75% não entra. Limites inferiores são inclusivos.
Quando o instrumento fornece VolumeStep positivo, arredonda o tamanho para baixo até esse passo. Tamanho zero ou fora dos limites mínimo/máximo fornecidos é rejeitado. Não aumenta uma posição existente.
Com UseSession=true, o envio em horário de Nova York deve estar em [SessionStart, SessionEnd), padrão 04:00–09:30, observando horário de verão. Desativar a sessão mantém o limite diário. Uma entrada enviada consome a permissão da data, mesmo se a ordem falhar depois.
Manutenção e saída
O primeiro candle vermelho (close < open) ou com volume <= previousVolume fecha toda a posição comprada real. Candles de alta com volume crescente mantêm a posição; doji também mantém se o volume cresce. Saídas valem fora da sessão de entrada, independentemente de ganho, corpo ou volume mínimo. Não há nova entrada no candle de saída. Ordens pendentes bloqueiam duplicados; os valores anteriores continuam atualizados em todo candle concluído.
Sem indicadores, entradas vendidas, intervalo obrigatório de seis horas ou stop-loss/take-profit separados. Outra entrada exige a próxima data elegível de Nova York.
Valores padrão
Parâmetro
Padrão
Significado
MinGainPct
5
Ganho percentual mínimo inclusivo; não negativo
MinVolume
15000
Volume mínimo estrito; não negativo, nas unidades dos dados
UseSession
true
Filtro de horário de entrada; propriedade C# IsUseSession
SessionStart / SessionEnd
04:00 / 09:30
Horários ordenados no mesmo dia de Nova York, início incluído e fim excluído
CandleType
5 minutos
Candles de sinal concluídos
Testes usam o arquivo BTC/TON existente para verificar cada ordem/posição real, três tamanhos de corpo, arredondamento de lotes, limite diário, ambas as saídas, manutenções e transição primaveral do horário de verão. TON tem candle válido com os valores publicados 5%/15000; testes mecânicos BTC ajustam explicitamente os limiares. Não demonstram lucratividade.
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>
/// Opens one long momentum trade per New York day, sizes it by candle body,
/// and exits on a red candle or non-increasing candle volume.
/// </summary>
public class PremarketGapMomoTraderStrategy : Strategy
{
private readonly StrategyParam<decimal> _minGainPct;
private readonly StrategyParam<decimal> _minVolume;
private readonly StrategyParam<bool> _useSession;
private readonly StrategyParam<TimeSpan> _sessionStart;
private readonly StrategyParam<TimeSpan> _sessionEnd;
private readonly StrategyParam<DataType> _candleType;
private decimal _previousClose;
private decimal _previousVolume;
private bool _isInitialized;
private DateTime? _lastEntryDate;
private Order _pendingOrder;
/// <summary>Initializes gain 5%, minimum candle volume 15000 and the 04:00–09:30 New York entry session.</summary>
public PremarketGapMomoTraderStrategy()
{
_minGainPct = Param(nameof(MinGainPct), 5m).SetRange(0m, decimal.MaxValue)
.SetDisplay("Minimum Gain Percent", "Inclusive close gain over the preceding finished candle", "Entry");
_minVolume = Param(nameof(MinVolume), 15000m).SetRange(0m, decimal.MaxValue)
.SetDisplay("Minimum Candle Volume", "Strict lower bound for candle volume", "Entry");
_useSession = Param("UseSession", true)
.SetDisplay("Use Session", "Restrict entries to the New York premarket session", "Session");
_sessionStart = Param(nameof(SessionStart), TimeSpan.FromHours(4))
.SetDisplay("Session Start", "Inclusive New York entry time", "Session");
_sessionEnd = Param(nameof(SessionEnd), TimeSpan.FromMinutes(570))
.SetDisplay("Session End", "Exclusive New York entry time", "Session");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished candles supplying price, body and volume", "General");
OrderRegistering += order => _pendingOrder = order;
}
/// <summary>Inclusive minimum close-to-previous-close gain in percent.</summary>
public decimal MinGainPct { get => _minGainPct.Value; set => _minGainPct.Value = value; }
/// <summary>Strict minimum finished-candle volume.</summary>
public decimal MinVolume { get => _minVolume.Value; set => _minVolume.Value = value; }
/// <summary>Whether entry times are restricted; the published parameter key is UseSession.</summary>
public bool IsUseSession { get => _useSession.Value; set => _useSession.Value = value; }
/// <summary>Inclusive New York entry time, before SessionEnd.</summary>
public TimeSpan SessionStart { get => _sessionStart.Value; set => _sessionStart.Value = value; }
/// <summary>Exclusive New York entry time within the same day.</summary>
public TimeSpan SessionEnd { get => _sessionEnd.Value; set => _sessionEnd.Value = value; }
/// <summary>Finished candle type; five minutes by default.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
private void ResetState()
{
_previousClose = _previousVolume = 0m;
_isInitialized = false;
_lastEntryDate = null;
_pendingOrder = null;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (MinGainPct < 0m || MinVolume < 0m || SessionStart < TimeSpan.Zero || SessionEnd > TimeSpan.FromDays(1)
|| SessionStart >= SessionEnd)
throw new ArgumentException("Gain and volume thresholds cannot be negative; session times must be ordered within one New York day.");
ResetState();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void Submit(Sides side, decimal volume)
=> RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = side == Sides.Buy ? "Premarket body momentum" : "Premarket momentum exit",
});
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var previousClose = _previousClose;
var previousVolume = _previousVolume;
_previousClose = candle.ClosePrice;
_previousVolume = candle.TotalVolume;
if (!_isInitialized)
{
_isInitialized = true;
return;
}
if (!IsFormedAndOnlineAndAllowTrading()
|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m)
{
if (candle.ClosePrice < candle.OpenPrice || candle.TotalVolume <= previousVolume)
Submit(Sides.Sell, Position);
return;
}
if (Position != 0m || previousClose <= 0m || candle.ClosePrice <= candle.OpenPrice
|| candle.TotalVolume <= MinVolume || candle.HighPrice <= candle.LowPrice
|| 100m * (candle.ClosePrice - previousClose) < MinGainPct * previousClose)
return;
var bodyPercent = 100m * (candle.ClosePrice - candle.OpenPrice) / (candle.HighPrice - candle.LowPrice);
var fraction = bodyPercent >= 90m ? 1m : bodyPercent >= 85m ? 0.5m : bodyPercent >= 75m ? 0.25m : 0m;
if (fraction == 0m)
return;
var local = TimeZoneInfo.ConvertTimeFromUtc(CurrentTime, TimeHelper.Est);
if (_lastEntryDate == local.Date || IsUseSession && (local.TimeOfDay < SessionStart || local.TimeOfDay >= SessionEnd))
return;
var volume = Volume * fraction;
if (Security.VolumeStep is decimal step && step > 0m)
volume = volume.Floor(step);
if (volume <= 0m || Security.MinVolume is decimal min && volume < min
|| Security.MaxVolume is decimal max && max > 0m && volume > max)
return;
_lastEntryDate = local.Date;
Submit(Sides.Buy, volume);
}
}
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 ArgumentException, Decimal, TimeSpan, TimeZoneInfo
from Ecng.Common import MathHelper, TimeHelper
from StockSharp.Messages import CandleStates, DataType, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order
from StockSharp.Algo.Strategies import Strategy
class premarket_gap_momo_trader_strategy(Strategy):
def __init__(self):
super(premarket_gap_momo_trader_strategy, self).__init__()
self._min_gain_pct = self.Param("MinGainPct", Decimal(5)).SetRange(Decimal.Zero, Decimal.MaxValue) \
.SetDisplay("Minimum Gain Percent", "Inclusive close gain over the preceding finished candle", "Entry")
self._min_volume = self.Param("MinVolume", Decimal(15000)).SetRange(Decimal.Zero, Decimal.MaxValue) \
.SetDisplay("Minimum Candle Volume", "Strict lower bound for candle volume", "Entry")
self._use_session = self.Param("UseSession", True) \
.SetDisplay("Use Session", "Restrict entries to the New York premarket session", "Session")
self._session_start = self.Param("SessionStart", TimeSpan.FromHours(4)) \
.SetDisplay("Session Start", "Inclusive New York entry time", "Session")
self._session_end = self.Param("SessionEnd", TimeSpan.FromMinutes(570)) \
.SetDisplay("Session End", "Exclusive New York entry time", "Session")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying price, body and volume", "General")
self._reset_state()
self.OrderRegistering += self._observe_order
@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._previous_close = self._previous_volume = Decimal.Zero
self._is_initialized = False
self._last_entry_date = None
self._pending_order = None
def OnReseted(self):
super(premarket_gap_momo_trader_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(premarket_gap_momo_trader_strategy, self).OnStarted2(time)
if self._min_gain_pct.Value < 0 or self._min_volume.Value < 0 \
or self._session_start.Value < TimeSpan.Zero or self._session_end.Value > TimeSpan.FromDays(1) \
or self._session_start.Value >= self._session_end.Value:
raise ArgumentException("Gain and volume thresholds cannot be negative; session times must be ordered within one New York day.")
self._reset_state()
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 _submit(self, side, volume):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = "Premarket body momentum" if side == Sides.Buy else "Premarket momentum exit"
self.RegisterOrder(order)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
previous_close = self._previous_close
previous_volume = self._previous_volume
self._previous_close = candle.ClosePrice
self._previous_volume = candle.TotalVolume
if not self._is_initialized:
self._is_initialized = True
return
pending = self._pending_order
if not self.IsFormedAndOnlineAndAllowTrading() \
or pending is not None and pending.State != OrderStates.Done and pending.State != OrderStates.Failed:
return
if self.Position > 0:
if candle.ClosePrice < candle.OpenPrice or candle.TotalVolume <= previous_volume:
self._submit(Sides.Sell, self.Position)
return
if self.Position != 0 or previous_close <= 0 or candle.ClosePrice <= candle.OpenPrice \
or candle.TotalVolume <= self._min_volume.Value or candle.HighPrice <= candle.LowPrice \
or Decimal(100) * (candle.ClosePrice - previous_close) < self._min_gain_pct.Value * previous_close:
return
body_percent = Decimal(100) * (candle.ClosePrice - candle.OpenPrice) / (candle.HighPrice - candle.LowPrice)
fraction = Decimal.One if body_percent >= 90 else Decimal.One / Decimal(2) if body_percent >= 85 \
else Decimal.One / Decimal(4) if body_percent >= 75 else Decimal.Zero
if fraction == 0:
return
local = TimeZoneInfo.ConvertTimeFromUtc(self.CurrentTime, TimeHelper.Est)
if self._last_entry_date == local.Date or self._use_session.Value \
and (local.TimeOfDay < self._session_start.Value or local.TimeOfDay >= self._session_end.Value):
return
volume = self.Volume * fraction
step = self.Security.VolumeStep
if step is not None and step > 0:
volume = MathHelper.Floor(volume, step)
minimum = self.Security.MinVolume
maximum = self.Security.MaxVolume
if volume <= 0 or minimum is not None and volume < minimum \
or maximum is not None and maximum > 0 and volume > maximum:
return
self._last_entry_date = local.Date
self._submit(Sides.Buy, volume)
def CreateClone(self):
return premarket_gap_momo_trader_strategy()