Premarket Gap MomoTrader
只做多的蜡烛动量示例,每个纽约日历日最多入场一次。“跳空”指当前close相对前一close上涨,不要求隔夜开盘跳空或EMA交叉。
入场和仓位
只使用已完成的蜡烛,默认五分钟。第一根记录前一close和成交量。从空仓入场要求严格阳线、正的high-low范围、总成交量 > MinVolume、正的前一close,且 100 * (close - previousClose) / previousClose >= MinGainPct。
bodyPercent = 100 * (close - open) / (high - low):≥90%使用策略Volume的100%;≥85%且<90%使用50%;≥75%且<85%使用25%;<75%不入场,边界包含。
若证券提供正的VolumeStep,数量向下取整至该步长。零数量或超出证券已提供最小/最大数量的订单被拒绝。不增加已有持仓。
UseSession=true时,提交时间必须在纽约[SessionStart, SessionEnd)内,默认04:00–09:30,考虑夏令时。禁用时段过滤仍保留每日一次限制。提交入场订单即用完该日机会,即使订单随后失败。
持有和退出
入场后,第一根阴线(close < open)或volume <= previousVolume的蜡烛完全平掉实际多头。阳线且成交量增加时继续持有;十字星在成交量增加时也持有。退出不受入场时段、涨幅、实体比例或最小成交量限制。退出蜡烛不同时再次入场。未完成订单阻止重复提交,但每根已完成蜡烛仍更新前一close和成交量。
没有指标、空头入场、六小时冷却、独立止损或止盈。下次入场须等到下一个符合条件的纽约日期。
默认值
| 参数 | 默认值 | 含义 |
|---|---|---|
MinGainPct |
5 | 包含边界的涨幅百分比下限,非负 |
MinVolume |
15000 | 严格的蜡烛成交量下限,非负,使用数据源单位 |
UseSession |
true | 入场时段过滤;C#属性IsUseSession |
SessionStart / SessionEnd |
04:00 / 09:30 | 纽约同一天内按顺序的时间,含起点、不含终点 |
CandleType |
5分钟 | 已完成信号蜡烛 |
测试仅使用现有BTC/TON历史,检查每个订单和实际持仓、三个实体比例档位、数量步长、每日限制、两种退出、上涨持有和春季纽约夏令时变化。TON存在符合原始5%/15000设置的蜡烛;BTC机制测试明确调整阈值。不证明盈利。
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()