Premarket Gap MomoTrader
ロング専用の足の勢いを使う例で、ニューヨークの暦日ごとに1回だけ入ります。「ギャップ」は前回終値から現在終値への上昇を指し、夜間の始値ギャップやEMAクロスは不要です。
エントリーと数量
確定足のみを使い、既定は5分足です。最初の足は前回終値と出来高を保存します。ポジションなしからのエントリーは、厳密な陽線、正のhigh-low幅、総出来高 > MinVolume、正の前回終値、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の足で実際のロング全量を決済します。陽線かつ出来高増加なら保有し、同時線も出来高増加なら保有します。決済はエントリー時間帯の外でも判定し、上昇率・実体比率・最低出来高の条件に依存しません。決済した足で再エントリーしません。未完了注文は重複を防ぎますが、前回終値と出来高は毎確定足で更新します。
指標、ショートのエントリー、6時間待機、個別のストップロスや利確はありません。次のエントリーには次の条件を満たすニューヨーク日付が必要です。
既定値
| パラメーター | 既定値 | 意味 |
|---|---|---|
MinGainPct |
5 | 上昇率の下限百分率。境界を含み、非負 |
MinVolume |
15000 | 厳密な出来高下限。非負でデータの単位を使う |
UseSession |
true | エントリー時刻フィルター。C#プロパティIsUseSession |
SessionStart / SessionEnd |
04:00 / 09:30 | 同じニューヨーク日の順序付き時刻。開始を含み、終了を含まない |
CandleType |
5分 | 確定シグナル足 |
既存BTC/TON履歴を使い、各注文と実際のポジション、3段階の数量、ロット切り下げ、日次制限、2種類の決済、保有、春のニューヨーク夏時間移行をテストします。公開既定値5%/15000にはTONで適合足があり、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()