FitFul 13 с временными фильтрами — порт эксперта MetaTrader 4 «FitFul_13» на платформу StockSharp. Стратегия строит лестницу недельных пивотов (PP, R0.5, R1, R1.5, R2, R2.5, R3 и симметричные уровни поддержки) по максимуму, минимуму и закрытию предыдущей недели. Торговые решения принимаются на основном таймфрейме (по умолчанию 1 час) и подтверждаются более быстрым таймфреймом (по умолчанию 15 минут). Новые позиции разрешены только в заданные минуты часа, что повторяет логику исходного советника.
Логика сигналов
Расчёт недельных пивотов
После закрытия каждой недельной свечи пересчитывается весь набор уровней.
Стоп и тейк рассчитываются от базовых уровней с учётом настраиваемого отступа в пунктах.
Условия на основном таймфрейме
Последняя завершённая свеча должна быть бычьей для поиска лонга или медвежьей для поиска шорта.
Предыдущая свеча обязана пересекать соответствующий уровень пивота (для лонга — открыть ниже и закрыть выше, для шорта наоборот).
Условия на подтверждающем таймфрейме
При бычьей текущей подтверждающей свече минимумы двух предыдущих свечей должны пробивать уровень и закрываться выше него.
При медвежьей подтверждающей свече максимумы двух предыдущих свечей должны пробивать уровень и закрываться ниже него.
Временные фильтры
Сделка открывается только если минута открытия завершённой основной свечи равна одному из четырёх настроенных значений (по умолчанию 0, 15, 30 или 45).
Чистая позиция ограничена значением MaxNetPositions × Volume, что имитирует максимум три ордера у версии MT4.
Управление рисками
Стоп и тейк — назначаются сразу после открытия позиции, используя ближайшие уровни пивотов.
Трейлинг-стоп — при движении цены на заданное количество пунктов стоп подтягивается в сторону прибыли.
Ограничение по времени — прибыльные позиции закрываются, если время удержания превысило установленный порог (по умолчанию 48 часов).
Правило пятницы — в пятницу все позиции закрываются в выбранном интервале времени (по умолчанию 21:50–21:59).
Параметры
Имя
Описание
PrimaryCandleType
Таймфрейм для поиска пересечения пивотов.
ConfirmationCandleType
Быстрый таймфрейм для подтверждения реакции на уровень.
Volume
Объём рыночной заявки.
MaxNetPositions
Максимальная чистая позиция в кратных значениях Volume.
OffsetPoints
Отступ в пунктах для расчёта стопов и тейков.
TrailingStopPoints
Размер трейлинг-стопа в пунктах.
CloseAfter
Максимальное время удержания прибыльной позиции.
CloseHour, CloseMinuteFrom, CloseMinuteTo
Временное окно принудительного закрытия в пятницу.
EntryMinute0..3
Допустимые минуты часа для открытия новых позиций.
Примечания
В портированной версии сохранены недельные пивоты и дискретный тайминг входов оригинального советника.
Управление капиталом упрощено: параметр Volume напрямую задаёт объём сделки вместо сложного расчёта лота в MT4.
Все комментарии в коде написаны на английском языке согласно требованиям репозитория.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// FitFul 13 Time Gated strategy - channel midpoint crossover.
/// Buys when close crosses above the midpoint of Highest/Lowest channel.
/// Sells when close crosses below the midpoint.
/// </summary>
public class FitFul13TimeGatedStrategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevMid;
private bool _hasPrev;
public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public FitFul13TimeGatedStrategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 13)
.SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators");
_emaPeriod = Param(nameof(EmaPeriod), 13)
.SetDisplay("EMA Period", "EMA trend filter", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevClose = 0m; _prevMid = 0m; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var highest = new Highest { Length = ChannelPeriod };
var lowest = new Lowest { Length = ChannelPeriod };
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(highest, lowest, ema, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest, decimal ema)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var mid = (highest + lowest) / 2;
if (!_hasPrev)
{
_prevClose = close;
_prevMid = mid;
_hasPrev = true;
return;
}
// Cross above midpoint with EMA confirmation
if (_prevClose <= _prevMid && close > mid && close > ema && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Cross below midpoint with EMA confirmation
else if (_prevClose >= _prevMid && close < mid && close < ema && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
_prevClose = close;
_prevMid = mid;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class fit_ful13_time_gated_strategy(Strategy):
def __init__(self):
super(fit_ful13_time_gated_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 13) \
.SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators")
self._ema_period = self.Param("EmaPeriod", 13) \
.SetDisplay("EMA Period", "EMA trend filter", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_close = 0.0
self._prev_mid = 0.0
self._has_prev = False
@property
def channel_period(self):
return self._channel_period.Value
@property
def ema_period(self):
return self._ema_period.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(fit_ful13_time_gated_strategy, self).OnReseted()
self._prev_close = 0.0
self._prev_mid = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(fit_ful13_time_gated_strategy, self).OnStarted2(time)
self._has_prev = False
highest = Highest()
highest.Length = self.channel_period
lowest = Lowest()
lowest.Length = self.channel_period
ema = ExponentialMovingAverage()
ema.Length = self.ema_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(highest, lowest, ema, self.process_candle).Start()
def process_candle(self, candle, highest, lowest, ema):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high_val = float(highest)
low_val = float(lowest)
ema_val = float(ema)
mid = (high_val + low_val) / 2.0
if not self._has_prev:
self._prev_close = close
self._prev_mid = mid
self._has_prev = True
return
if self._prev_close <= self._prev_mid and close > mid and close > ema_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif self._prev_close >= self._prev_mid and close < mid and close < ema_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_close = close
self._prev_mid = mid
def CreateClone(self):
return fit_ful13_time_gated_strategy()