Стратегия PowerHouse SwiftEdge AI v2.10
Стратегия пробоя пивотов с фильтром импульса и простым управлением рисками. Длинная позиция открывается при пробое последнего пивота вверх при достаточном импульсе и минимальном интервале между сигналами. Короткая позиция открывается при пробое пивота вниз и отрицательном импульсе. Позиции закрываются по тейк‑профиту и стоп‑лоссу в пунктах.
Расчёт и исполнение
Это детерминированный пример: историческое название не подразумевает обученную ИИ-модель. C# и Python одинаково реализуют следующее явное уточнение описания выше.
Используются завершённые базовые свечи. Пивотный максимум строго выше highs PivotPeriod соседних баров с каждой стороны; минимум строго ниже их lows. Равенство исключает пивот. Подтверждение поступает лишь после завершения правых баров, без сигналов задним числом. Последние подтверждённые максимум и минимум хранятся независимо.
Лонг требует предыдущий Close <= пивотный максимум и текущий Close > пивотный максимум; шорт — обратного пересечения пивотного минимума. Импульс равен текущий Close - Close[MomentumPeriod баров назад]. Для лонга он строго выше MomentumThreshold, для шорта — строго ниже отрицательного порога. Для обеих сторон нужно не менее MinSignalBars завершённых баров после последнего принятого входа; ноль отключает интервал. Отклонённые сигналы не откладываются. EMA-фильтра и шестичасового cooldown нет.
Из нулевой позиции открывается Volume, встречная полностью разворачивается объёмом Volume + abs(Position). Ордер в работе блокирует новые ордера. Стоп и тейк заданы фиксированными единицами цены котировки: один пункт — разница цены 1, а не тик, множитель PriceStep или процент. Значения нужно настроить под инструмент. Основа — фактическая цена исполнения, средневзвешенная при частичном увеличении той же стороны; уменьшение сохраняет основу, разворот начинает новую.
Локальная защита рыночным ордером закрывает весь объём при достижении стопа или тейка текущим bid для лонга либо ask для шорта. Закрытия завершённых свечей тоже проверяют защиту; касания high/low не выдаются за упорядоченные внутрисвечные события. Защита не ограничена интервалом входов и приоритетна перед новым входом на той же свече. При разрывах цена исполнения может отличаться от уровня срабатывания.
Значения по умолчанию: PivotPeriod=2, MomentumPeriod=10, MomentumThreshold=0, MinSignalBars=5, StopLossPoints=50, TakeProfitPoints=100, CandleType=TimeSpan.FromMinutes(5).TimeFrame(). Периоды и оба расстояния выхода положительны; порог импульса и интервал неотрицательны.
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 confirmed-pivot breakouts with signed close momentum and finished-bar spacing,
/// protected by fixed price-point distances from actual fill prices.
/// </summary>
public class PowerHouseSwiftEdgeAiV210Strategy : Strategy
{
private readonly StrategyParam<int> _pivotPeriod;
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<decimal> _momentumThreshold;
private readonly StrategyParam<int> _minSignalBars;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal high, decimal low, decimal close)> _bars = [];
private decimal? _pivotHigh;
private decimal? _pivotLow;
private long _barCount;
private long? _lastSignal;
private Order _pendingOrder;
private decimal _filledPosition;
private decimal _entryPrice;
/// <summary>Initializes two-bar pivots, ten-bar momentum, five-bar spacing and 50/100 price-point exits.</summary>
public PowerHouseSwiftEdgeAiV210Strategy()
{
_pivotPeriod = Param(nameof(PivotPeriod), 2).SetGreaterThanZero()
.SetDisplay("Pivot Period", "Strict neighboring bars on each side of a confirmed pivot", "Signals");
_momentumPeriod = Param(nameof(MomentumPeriod), 10).SetGreaterThanZero()
.SetDisplay("Momentum Period", "Finished-bar close lookback", "Signals");
_momentumThreshold = Param(nameof(MomentumThreshold), 0m).SetRange(0m, decimal.MaxValue)
.SetDisplay("Momentum Threshold", "Minimum signed close change in absolute price units", "Signals");
_minSignalBars = Param(nameof(MinSignalBars), 5).SetRange(0, int.MaxValue)
.SetDisplay("Minimum Signal Bars", "Finished-bar interval between accepted entries; zero disables spacing", "Signals");
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetGreaterThanZero()
.SetDisplay("Stop Loss Points", "Fixed absolute price distance from the actual fill basis", "Protection");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 100m).SetGreaterThanZero()
.SetDisplay("Take Profit Points", "Fixed absolute price distance from the actual fill basis", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished candles supplying pivots, momentum and spacing", "General");
OrderRegistering += order => _pendingOrder = order;
OwnTradeReceived += ObserveFill;
}
/// <summary>Strict neighboring bars on each side of a pivot.</summary>
public int PivotPeriod { get => _pivotPeriod.Value; set => _pivotPeriod.Value = value; }
/// <summary>Lookback for the signed close-price difference.</summary>
public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }
/// <summary>Nonnegative absolute-price threshold, exceeded strictly in the entry direction.</summary>
public decimal MomentumThreshold { get => _momentumThreshold.Value; set => _momentumThreshold.Value = value; }
/// <summary>Minimum finished-bar interval between accepted entry orders; zero disables it.</summary>
public int MinSignalBars { get => _minSignalBars.Value; set => _minSignalBars.Value = value; }
/// <summary>Stop distance in absolute quote-price points, not ticks or percent.</summary>
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
/// <summary>Take-profit distance in absolute quote-price points, not ticks or percent.</summary>
public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
/// <summary>Base 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), (Security, DataType.Level1)];
private void ResetState()
{
_bars.Clear();
_pivotHigh = _pivotLow = null;
_barCount = 0;
_lastSignal = null;
_pendingOrder = null;
_filledPosition = _entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (PivotPeriod <= 0 || MomentumPeriod <= 0 || MomentumThreshold < 0m
|| MinSignalBars < 0 || StopLossPoints <= 0m || TakeProfitPoints <= 0m)
throw new ArgumentException("Pivot/momentum periods and point distances must be positive; thresholds and spacing cannot be negative.");
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();
}
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 void ObserveFill(Subscription subscription, MyTrade trade)
{
var previous = _filledPosition;
var signed = trade.Order.Side == Sides.Buy ? trade.Trade.Volume : -trade.Trade.Volume;
_filledPosition += signed;
if (_filledPosition == 0m)
_entryPrice = 0m;
else if (previous == 0m || Math.Sign(_filledPosition) != Math.Sign(previous))
_entryPrice = trade.Trade.Price;
else if (Math.Sign(signed) == Math.Sign(previous))
_entryPrice = (_entryPrice * Math.Abs(previous) + trade.Trade.Price * Math.Abs(signed)) / Math.Abs(_filledPosition);
}
private bool TryRiskExit(decimal price)
{
if (Position == 0m || _entryPrice <= 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return false;
var profit = (price - _entryPrice) * Math.Sign(Position);
var comment = profit <= -StopLossPoints ? "SwiftEdge stop loss" : profit >= TakeProfitPoints ? "SwiftEdge take profit" : null;
if (comment is null)
return false;
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), comment);
return true;
}
private void ObserveQuote(Level1ChangeMessage quote)
{
var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (quote.TryGetDecimal(field) is decimal price && price > 0m)
TryRiskExit(price);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_barCount++;
var width = checked(2 * PivotPeriod + 1);
_bars.Add((candle.HighPrice, candle.LowPrice, candle.ClosePrice));
if (_bars.Count > Math.Max(width, checked(MomentumPeriod + 1)))
_bars.RemoveAt(0);
if (_bars.Count >= width)
{
var start = _bars.Count - width;
var center = start + PivotPeriod;
var high = true;
var low = true;
for (var i = start; i < _bars.Count; i++)
{
if (i == center)
continue;
high &= _bars[center].high > _bars[i].high;
low &= _bars[center].low < _bars[i].low;
}
if (high)
_pivotHigh = _bars[center].high;
if (low)
_pivotLow = _bars[center].low;
}
if (TryRiskExit(candle.ClosePrice) || IsPending || !IsFormedAndOnlineAndAllowTrading()
|| _bars.Count <= MomentumPeriod || _bars.Count < 2
|| _lastSignal is long last && _barCount - last < MinSignalBars)
return;
var previous = _bars[^2].close;
var momentum = candle.ClosePrice - _bars[_bars.Count - MomentumPeriod - 1].close;
Sides? side = null;
if (_pivotHigh is decimal pivotHigh && previous <= pivotHigh && candle.ClosePrice > pivotHigh
&& momentum > MomentumThreshold && Position <= 0m)
side = Sides.Buy;
else if (_pivotLow is decimal pivotLow && previous >= pivotLow && candle.ClosePrice < pivotLow
&& momentum < -MomentumThreshold && Position >= 0m)
side = Sides.Sell;
if (side is Sides direction)
{
_lastSignal = _barCount;
Submit(direction, Volume + Math.Abs(Position), "SwiftEdge pivot breakout");
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import ArgumentException, Decimal, Int32, Math, TimeSpan
from StockSharp.Messages import CandleStates, DataType, ITickTradeMessage, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class power_house_swift_edge_ai_v2_10_strategy(Strategy):
def __init__(self):
super(power_house_swift_edge_ai_v2_10_strategy, self).__init__()
self._pivot_period = self.Param("PivotPeriod", 2).SetGreaterThanZero() \
.SetDisplay("Pivot Period", "Strict neighboring bars on each side of a confirmed pivot", "Signals")
self._momentum_period = self.Param("MomentumPeriod", 10).SetGreaterThanZero() \
.SetDisplay("Momentum Period", "Finished-bar close lookback", "Signals")
self._momentum_threshold = self.Param("MomentumThreshold", Decimal.Zero).SetRange(Decimal.Zero, Decimal.MaxValue) \
.SetDisplay("Momentum Threshold", "Minimum signed close change in absolute price units", "Signals")
self._min_signal_bars = self.Param("MinSignalBars", 5).SetRange(0, Int32.MaxValue) \
.SetDisplay("Minimum Signal Bars", "Finished-bar interval between accepted entries; zero disables spacing", "Signals")
self._stop_loss_points = self.Param("StopLossPoints", Decimal(50)).SetGreaterThanZero() \
.SetDisplay("Stop Loss Points", "Fixed absolute price distance from the actual fill basis", "Protection")
self._take_profit_points = self.Param("TakeProfitPoints", Decimal(100)).SetGreaterThanZero() \
.SetDisplay("Take Profit Points", "Fixed absolute price distance from the actual fill basis", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying pivots, momentum and spacing", "General")
tick_type = clr.GetClrType(ITickTradeMessage)
self._tick_price = tick_type.GetProperty("Price")
self._tick_volume = tick_type.GetProperty("Volume")
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._bars = []
self._pivot_high = self._pivot_low = None
self._bar_count = 0
self._last_signal = None
self._pending_order = None
self._filled_position = self._entry_price = Decimal.Zero
def OnReseted(self):
super(power_house_swift_edge_ai_v2_10_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(power_house_swift_edge_ai_v2_10_strategy, self).OnStarted2(time)
if self._pivot_period.Value <= 0 or self._momentum_period.Value <= 0 or self._momentum_threshold.Value < 0 \
or self._min_signal_bars.Value < 0 or self._stop_loss_points.Value <= 0 or self._take_profit_points.Value <= 0:
raise ArgumentException("Pivot/momentum periods and point distances must be positive; thresholds and spacing cannot be negative.")
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()
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 _observe_fill(self, subscription, trade):
previous = self._filled_position
price = self._tick_price.GetValue(trade.Trade)
volume = self._tick_volume.GetValue(trade.Trade)
signed = volume if trade.Order.Side == Sides.Buy else -volume
self._filled_position += signed
if self._filled_position == 0:
self._entry_price = Decimal.Zero
elif previous == 0 or Math.Sign(self._filled_position) != Math.Sign(previous):
self._entry_price = price
elif Math.Sign(signed) == Math.Sign(previous):
self._entry_price = (self._entry_price * abs(previous) + price * abs(signed)) / abs(self._filled_position)
def _try_risk_exit(self, price):
if self.Position == 0 or self._entry_price <= 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return False
profit = (price - self._entry_price) * Math.Sign(self.Position)
comment = "SwiftEdge stop loss" if profit <= -self._stop_loss_points.Value \
else "SwiftEdge take profit" if profit >= self._take_profit_points.Value else None
if comment is None:
return False
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), comment)
return True
def _observe_quote(self, quote):
field = Level1Fields.BestBidPrice if self.Position > 0 else Level1Fields.BestAskPrice
if quote.Changes.ContainsKey(field) and quote.Changes[field] > 0:
self._try_risk_exit(quote.Changes[field])
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
self._bar_count += 1
period = self._pivot_period.Value
momentum_period = self._momentum_period.Value
width = 2 * period + 1
self._bars.append((candle.HighPrice, candle.LowPrice, candle.ClosePrice))
if len(self._bars) > max(width, momentum_period + 1):
self._bars.pop(0)
if len(self._bars) >= width:
start = len(self._bars) - width
center = start + period
neighbors = [self._bars[index] for index in range(start, len(self._bars)) if index != center]
if all(self._bars[center][0] > bar[0] for bar in neighbors):
self._pivot_high = self._bars[center][0]
if all(self._bars[center][1] < bar[1] for bar in neighbors):
self._pivot_low = self._bars[center][1]
if self._try_risk_exit(candle.ClosePrice) or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading() \
or len(self._bars) <= momentum_period or len(self._bars) < 2 \
or self._last_signal is not None and self._bar_count - self._last_signal < self._min_signal_bars.Value:
return
previous = self._bars[-2][2]
momentum = candle.ClosePrice - self._bars[-momentum_period - 1][2]
side = None
if self._pivot_high is not None and previous <= self._pivot_high and candle.ClosePrice > self._pivot_high \
and momentum > self._momentum_threshold.Value and self.Position <= 0:
side = Sides.Buy
elif self._pivot_low is not None and previous >= self._pivot_low and candle.ClosePrice < self._pivot_low \
and momentum < -self._momentum_threshold.Value and self.Position >= 0:
side = Sides.Sell
if side is not None:
self._last_signal = self._bar_count
self._submit(side, self.Volume + abs(self.Position), "SwiftEdge pivot breakout")
def CreateClone(self):
return power_house_swift_edge_ai_v2_10_strategy()