Стратегия Ten Pips Opposite Last N Hour Trend
Общее описание
Стратегия является переносом эксперта MetaTrader 10pipsOnceADayOppositeLastNHourTrend. Она торгует ровно один раз в сутки в заранее заданный час и сознательно открывает позицию против движения цены за последние N завершённых часовых свечей. Логика рассчитана на валютные пары с пятизначными котировками, однако версия для StockSharp автоматически рассчитывает размер пункта на основании свойств инструмента (PriceStep и количество знаков после запятой).
В момент наступления торгового часа стратегия сравнивает цену закрытия HoursToCheckTrend часов назад с ценой закрытия последней законченной часовой свечи:
- Если более старая цена выше, значит рынок снижался, и открывается покупка.
- В противном случае рынок рос, и открывается продажа.
Выход из позиции осуществляется по защитным стопам, по ограничению времени удержания или при выходе цены за разрешённое торговое окно.
Управление объёмом
Модель управления капиталом повторяет мартингейл-лесенку оригинального эксперта:
- Базовый объём задаётся параметром
FixedVolume. Если он равен нулю, используется расчётPortfolio.CurrentValue * MaximumRisk / 1000с округлением до одной десятой лота. - Полученный объём ограничивается параметрами
MinimumVolume,MaximumVolume, лимитами биржи по инструменту и дополнительной планкойPortfolio.CurrentValue / 1000лотов. - После закрытия сделки её результат попадает в буфер (до пяти последних значений). При следующем входе стратегия просматривает этот буфер и умножает объём при первом найденном убытке, используя последовательность
FirstMultiplier…FifthMultiplier. Это полностью повторяет вложенныеOrderSelectиз MQL.
Управление рисками
StopLossPips,TakeProfitPipsиTrailingStopPipsзадаются в пунктах. Размер пункта вычисляется с учётом шага цены и количества знаков (для трёх и пяти знаков используется множитель ×10).- Трейлинг-стоп реализован симметрично для длинных и коротких позиций. В исходном коде МТ4 из-за знаковой ошибки трейлинг для шортов не срабатывал; в порте баг исправлен.
OrderMaxAgeзакрывает позицию, если она удерживается дольше заданного времени (по умолчанию 21 час).- Вне разрешённого торгового часа стратегия принудительно закрывает все позиции и ждёт следующего сигнала.
MaxOrdersне допускает повторного входа, пока открыта позиция или активны заявки.
Подробный алгоритм
- Подписка на свечи выбранного таймфрейма (по умолчанию 1 час, параметр
CandleType). - Сохранение цены закрытия каждой завершённой свечи в компактном буфере.
- При первой завершённой свече в разрешённый час:
- Проверка соединения и отсутствия позиций.
- Контроль, что имеется минимум
HoursToCheckTrendисторических свечей. - Определение направления по разнице текущего закрытия и закрытия
HoursToCheckTrendчасов назад. - Расчёт объёма с учётом мартингейла и отправка рыночной заявки.
- Пока позиция открыта:
- Проверка срабатывания стоп-лосса, тейк-профита и трейлинг-стопа по максимуму/минимуму свечи.
- Обновление уровня трейлинг-стопа при новых максимумах/минимумах.
- Отслеживание времени входа для контроля
OrderMaxAge. - Фиксация результата закрытой сделки для будущих умножений объёма.
Параметры
| Параметр | Описание | Значение по умолчанию |
|---|---|---|
FixedVolume |
Фиксированный объём. 0 включает расчёт от риска. |
0.1 |
MinimumVolume |
Минимально допустимый объём заявки. | 0.1 |
MaximumVolume |
Максимально допустимый объём заявки. | 5 |
MaximumRisk |
Доля капитала при FixedVolume = 0. |
0.05 |
MaxOrders |
Максимум одновременно открытых позиций/заявок. | 1 |
TradingHour |
Час (0–23), в который допускаются входы. | 7 |
HoursToCheckTrend |
Глубина истории в часах для определения направления. | 30 |
OrderMaxAge |
Предельное время удержания позиции. | 21 ч |
StopLossPips |
Дистанция стоп-лосса в пунктах. | 50 |
TakeProfitPips |
Дистанция тейк-профита в пунктах. | 10 |
TrailingStopPips |
Дистанция трейлинг-стопа в пунктах. | 0 (выключен) |
FirstMultiplier … FifthMultiplier |
Множители объёма при убытке на соответствующей глубине истории. | 4, 2, 5, 5, 1 |
CandleType |
Таймфрейм свечей для расчётов. | 1 час |
Отличия от MQL-версии
- Мартингейл, ограничение по времени и торговое окно реализованы идентично оригиналу. Единственное осознанное изменение — исправленный трейлинг-стоп для коротких позиций.
- Защитные уровни исполняются рыночными заявками при появлении следующей завершённой свечи. Это соответствует поведению MT4-эксперта в момент срабатывания стоп-заявок.
- Для оценки капитала используется
Portfolio.CurrentValue. Если адаптер не предоставляет это значение, берётся базовыйVolumeстратегии (по умолчанию1). - Массив торговых часов соответствует диапазону
0…23. Для ограничения торговли по дням недели можно изменить список_tradingDayHoursв конструкторе.
Рекомендации по применению
- Наиболее корректно стратегия работает на форекс-инструментах с часовыми свечами.
- Убедитесь, что у инструмента заданы
VolumeStep,VolumeMinиVolumeMax, иначе корректировка объёма может оказаться невозможной. - Запускайте стратегию заранее до наступления торгового часа, чтобы не пропустить единственный сигнал за день.
namespace StockSharp.Samples.Strategies;
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Trades once per day against the direction of the last N hourly candles.
/// Lot sizing mimics the martingale multipliers of the original MQL expert.
/// </summary>
public class TenPipsOppositeLastNHourTrendStrategy : Strategy
{
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _minimumVolume;
private readonly StrategyParam<decimal> _maximumVolume;
private readonly StrategyParam<decimal> _maximumRisk;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<int> _tradingHour;
private readonly StrategyParam<int> _hoursToCheckTrend;
private readonly StrategyParam<TimeSpan> _orderMaxAge;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _firstMultiplier;
private readonly StrategyParam<decimal> _secondMultiplier;
private readonly StrategyParam<decimal> _thirdMultiplier;
private readonly StrategyParam<decimal> _fourthMultiplier;
private readonly StrategyParam<decimal> _fifthMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<int> _tradingDayHours;
private readonly List<decimal> _closedTradeProfits = new();
private readonly List<decimal> _closeHistory = new();
private decimal _pipSize;
private DateTimeOffset? _lastBarTraded;
private Sides? _entrySide;
private decimal _entryVolume;
private decimal? _entryPrice;
private DateTimeOffset? _entryTime;
private decimal? _trailingStopPrice;
/// <summary>
/// Fixed volume for market entries. When zero the strategy uses risk based sizing.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Minimum allowed volume after all adjustments.
/// </summary>
public decimal MinimumVolume
{
get => _minimumVolume.Value;
set => _minimumVolume.Value = value;
}
/// <summary>
/// Maximum allowed volume after all adjustments.
/// </summary>
public decimal MaximumVolume
{
get => _maximumVolume.Value;
set => _maximumVolume.Value = value;
}
/// <summary>
/// Fraction of account value risked when FixedVolume is zero.
/// </summary>
public decimal MaximumRisk
{
get => _maximumRisk.Value;
set => _maximumRisk.Value = value;
}
/// <summary>
/// Maximum number of simultaneously open orders and positions.
/// </summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>
/// Hour (0-23) when the strategy is allowed to open a trade.
/// </summary>
public int TradingHour
{
get => _tradingHour.Value;
set => _tradingHour.Value = value;
}
/// <summary>
/// Number of hours used to evaluate the opposite trend.
/// </summary>
public int HoursToCheckTrend
{
get => _hoursToCheckTrend.Value;
set => _hoursToCheckTrend.Value = value;
}
/// <summary>
/// Maximum allowed lifetime for an open position.
/// </summary>
public TimeSpan OrderMaxAge
{
get => _orderMaxAge.Value;
set => _orderMaxAge.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing-stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Multiplier applied after the most recent losing trade.
/// </summary>
public decimal FirstMultiplier
{
get => _firstMultiplier.Value;
set => _firstMultiplier.Value = value;
}
/// <summary>
/// Multiplier applied when the last trade was profitable but the previous one lost.
/// </summary>
public decimal SecondMultiplier
{
get => _secondMultiplier.Value;
set => _secondMultiplier.Value = value;
}
/// <summary>
/// Multiplier applied when only the third most recent trade lost.
/// </summary>
public decimal ThirdMultiplier
{
get => _thirdMultiplier.Value;
set => _thirdMultiplier.Value = value;
}
/// <summary>
/// Multiplier applied when only the fourth most recent trade lost.
/// </summary>
public decimal FourthMultiplier
{
get => _fourthMultiplier.Value;
set => _fourthMultiplier.Value = value;
}
/// <summary>
/// Multiplier applied when only the fifth most recent trade lost.
/// </summary>
public decimal FifthMultiplier
{
get => _fifthMultiplier.Value;
set => _fifthMultiplier.Value = value;
}
/// <summary>
/// Type of candles processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TenPipsOppositeLastNHourTrendStrategy"/> class.
/// </summary>
public TenPipsOppositeLastNHourTrendStrategy()
{
_fixedVolume = Param(nameof(FixedVolume), 0.1m)
.SetDisplay("Fixed Volume", "Fixed volume for entries", "Risk")
.SetOptimize(0m, 1m, 0.1m);
_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
.SetDisplay("Minimum Volume", "Minimum allowed volume", "Risk");
_maximumVolume = Param(nameof(MaximumVolume), 5m)
.SetDisplay("Maximum Volume", "Maximum allowed volume", "Risk");
_maximumRisk = Param(nameof(MaximumRisk), 0.05m)
.SetDisplay("Maximum Risk", "Risk fraction when Fixed Volume is zero", "Risk")
.SetOptimize(0m, 0.2m, 0.01m);
_maxOrders = Param(nameof(MaxOrders), 1)
.SetDisplay("Max Orders", "Maximum simultaneous orders", "Trading")
.SetOptimize(1, 3, 1);
_tradingHour = Param(nameof(TradingHour), 7)
.SetDisplay("Trading Hour", "Hour when entries are allowed", "Trading");
_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 30)
.SetDisplay("Hours To Check Trend", "Look-back hours for trend detection", "Trading")
.SetGreaterThanZero();
_orderMaxAge = Param(nameof(OrderMaxAge), TimeSpan.FromSeconds(75600))
.SetDisplay("Order Max Age", "Maximum position lifetime", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 0m)
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk");
_firstMultiplier = Param(nameof(FirstMultiplier), 4m)
.SetDisplay("First Multiplier", "Multiplier after the last loss", "Money Management");
_secondMultiplier = Param(nameof(SecondMultiplier), 2m)
.SetDisplay("Second Multiplier", "Multiplier if only the previous trade lost", "Money Management");
_thirdMultiplier = Param(nameof(ThirdMultiplier), 5m)
.SetDisplay("Third Multiplier", "Multiplier if only the third trade lost", "Money Management");
_fourthMultiplier = Param(nameof(FourthMultiplier), 5m)
.SetDisplay("Fourth Multiplier", "Multiplier if only the fourth trade lost", "Money Management");
_fifthMultiplier = Param(nameof(FifthMultiplier), 1m)
.SetDisplay("Fifth Multiplier", "Multiplier if only the fifth trade lost", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for analysis", "Trading");
_tradingDayHours = new List<int>(24);
for (var hour = 0; hour < 24; hour++)
_tradingDayHours.Add(hour);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closedTradeProfits.Clear();
_closeHistory.Clear();
_lastBarTraded = null;
_entrySide = null;
_entryVolume = 0m;
_entryPrice = null;
_entryTime = null;
_trailingStopPrice = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
InitializePipSize();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
UpdateCloseHistory(candle.ClosePrice);
if (Position != 0 && UpdateProtectiveLogic(candle))
return;
if (Position != 0 && CloseExpiredPosition(candle.CloseTime))
return;
if (!IsTradingHour(candle.CloseTime))
{
FlattenOutsideTradingHours();
return;
}
if (!HasTrendSample())
return;
if (!CanOpenOnBar(candle.OpenTime))
return;
if (Position != 0)
return;
var direction = DetermineDirection();
if (direction == 0)
return;
var volume = CalculateOrderVolume(candle.ClosePrice);
if (volume <= 0m)
return;
if (direction > 0)
{
// Enter long against a bearish move in the look-back window.
BuyMarket(volume);
}
else
{
// Enter short against a bullish move in the look-back window.
SellMarket(volume);
}
_lastBarTraded = candle.OpenTime;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order == null || trade.Trade == null)
return;
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
var time = trade.Trade.ServerTime;
if (volume <= 0m || price <= 0m)
return;
if (_entrySide == null || _entrySide == trade.Order.Side)
{
RegisterEntryTrade(price, volume, trade.Order.Side, time);
}
else
{
RegisterExitTrade(price, volume, time);
}
}
private void RegisterEntryTrade(decimal price, decimal volume, Sides side, DateTimeOffset time)
{
// Weighted-average entry price for pyramided fills.
var totalVolume = _entryVolume + volume;
if (totalVolume <= 0m)
{
_entryVolume = 0m;
_entryPrice = null;
_entrySide = null;
_entryTime = null;
_trailingStopPrice = null;
return;
}
_entryPrice = _entryVolume > 0m && _entryPrice.HasValue
? ((_entryPrice.Value * _entryVolume) + (price * volume)) / totalVolume
: price;
_entryVolume = totalVolume;
_entrySide = side;
_entryTime ??= time;
var trailingDistance = GetTrailingDistance();
if (TrailingStopPips > 0m && trailingDistance > 0m)
{
_trailingStopPrice = side == Sides.Buy
? _entryPrice - trailingDistance
: _entryPrice + trailingDistance;
}
}
private void RegisterExitTrade(decimal price, decimal volume, DateTimeOffset time)
{
if (_entrySide == null || !_entryPrice.HasValue || _entryVolume <= 0m)
return;
var remaining = _entryVolume - volume;
if (remaining < 0m)
remaining = 0m;
decimal profit = 0m;
if (_entrySide == Sides.Buy)
profit = (price - _entryPrice.Value) * volume;
else if (_entrySide == Sides.Sell)
profit = (_entryPrice.Value - price) * volume;
AddClosedTradeProfit(profit);
if (remaining == 0m)
{
ResetEntryState();
}
else
{
_entryVolume = remaining;
_entryTime = time;
}
}
private bool UpdateProtectiveLogic(ICandleMessage candle)
{
if (_entrySide == null || !_entryPrice.HasValue || _entryVolume <= 0m)
return false;
var pip = EnsurePipSize();
if (pip <= 0m)
return false;
var stopLoss = StopLossPips * pip;
var takeProfit = TakeProfitPips * pip;
var trailingDistance = TrailingStopPips * pip;
if (_entrySide == Sides.Buy)
{
if (StopLossPips > 0m && candle.LowPrice <= _entryPrice.Value - stopLoss)
{
SellMarket(Math.Abs(Position));
return true;
}
if (TakeProfitPips > 0m && candle.HighPrice >= _entryPrice.Value + takeProfit)
{
SellMarket(Math.Abs(Position));
return true;
}
if (TrailingStopPips > 0m && trailingDistance > 0m)
{
var candidate = candle.HighPrice - trailingDistance;
if (candidate > (_trailingStopPrice ?? decimal.MinValue) && candle.HighPrice - _entryPrice.Value > trailingDistance)
_trailingStopPrice = candidate;
if (_trailingStopPrice.HasValue && candle.LowPrice <= _trailingStopPrice.Value)
{
SellMarket(Math.Abs(Position));
return true;
}
}
}
else if (_entrySide == Sides.Sell)
{
if (StopLossPips > 0m && candle.HighPrice >= _entryPrice.Value + stopLoss)
{
BuyMarket(Math.Abs(Position));
return true;
}
if (TakeProfitPips > 0m && candle.LowPrice <= _entryPrice.Value - takeProfit)
{
BuyMarket(Math.Abs(Position));
return true;
}
if (TrailingStopPips > 0m && trailingDistance > 0m)
{
var candidate = candle.LowPrice + trailingDistance;
if (!_trailingStopPrice.HasValue || candidate < _trailingStopPrice.Value)
_trailingStopPrice = candidate;
if (_trailingStopPrice.HasValue && candle.HighPrice >= _trailingStopPrice.Value)
{
BuyMarket(Math.Abs(Position));
return true;
}
}
}
return false;
}
private bool CloseExpiredPosition(DateTimeOffset time)
{
if (OrderMaxAge <= TimeSpan.Zero || _entryTime == null)
return false;
if (time - _entryTime < OrderMaxAge)
return false;
if (Position > 0)
{
SellMarket(Math.Abs(Position));
return true;
}
if (Position < 0)
{
BuyMarket(Math.Abs(Position));
return true;
}
return false;
}
private bool IsTradingHour(DateTimeOffset time)
{
if (TradingHour < 0 || TradingHour > 23)
return false;
if (!_tradingDayHours.Contains(time.Hour))
return false;
return time.Hour == TradingHour;
}
private bool CanOpenOnBar(DateTimeOffset barOpenTime)
{
if (_lastBarTraded.HasValue && _lastBarTraded.Value == barOpenTime)
return false;
return true;
}
private void FlattenOutsideTradingHours()
{
if (Position > 0)
{
SellMarket(Math.Abs(Position));
}
else if (Position < 0)
{
BuyMarket(Math.Abs(Position));
}
}
private bool HasTrendSample()
{
return HoursToCheckTrend > 0 && _closeHistory.Count >= HoursToCheckTrend;
}
private int DetermineDirection()
{
if (_closeHistory.Count == 0)
return 0;
var latestIndex = _closeHistory.Count - 1;
var recentClose = _closeHistory[latestIndex];
var olderIndex = _closeHistory.Count - HoursToCheckTrend;
if (olderIndex < 0 || olderIndex >= _closeHistory.Count)
return 0;
var olderClose = _closeHistory[olderIndex];
return olderClose > recentClose ? 1 : -1;
}
private decimal CalculateOrderVolume(decimal price)
{
decimal baseVolume;
if (FixedVolume > 0m)
{
baseVolume = FixedVolume;
}
else
{
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity > 0m && MaximumRisk > 0m)
{
baseVolume = RoundToOneDecimal(equity * MaximumRisk / 1000m);
}
else
{
baseVolume = Volume > 0m ? Volume : 1m;
}
}
baseVolume = ApplyLossMultipliers(baseVolume);
var equityCap = Portfolio?.CurrentValue ?? 0m;
if (equityCap > 0m)
{
var cap = RoundToOneDecimal(equityCap / 1000m);
if (cap > 0m && baseVolume > cap)
baseVolume = cap;
}
if (baseVolume < MinimumVolume)
baseVolume = MinimumVolume;
else if (baseVolume > MaximumVolume)
baseVolume = MaximumVolume;
return AdjustVolume(baseVolume);
}
private decimal ApplyLossMultipliers(decimal volume)
{
if (_closedTradeProfits.Count == 0)
return volume;
var multipliers = new[]
{
FirstMultiplier,
SecondMultiplier,
ThirdMultiplier,
FourthMultiplier,
FifthMultiplier,
};
var count = _closedTradeProfits.Count;
for (var i = 0; i < multipliers.Length; i++)
{
if (count <= i)
break;
var profit = _closedTradeProfits[count - 1 - i];
if (profit < 0m)
{
volume *= multipliers[i];
break;
}
if (profit > 0m)
break;
}
return volume;
}
private decimal AdjustVolume(decimal volume)
{
var security = Security;
if (security != null)
{
var step = security.VolumeStep;
if (step is decimal stepValue && stepValue > 0m)
volume = Math.Round(volume / stepValue, MidpointRounding.AwayFromZero) * stepValue;
if (volume < 0.01m)
volume = 0.01m;
}
return volume > 0m ? volume : 0m;
}
private void UpdateCloseHistory(decimal close)
{
if (close <= 0m)
return;
_closeHistory.Add(close);
var maxLength = Math.Max(HoursToCheckTrend + 2, 64);
while (_closeHistory.Count > maxLength)
_closeHistory.RemoveAt(0);
}
private void AddClosedTradeProfit(decimal profit)
{
_closedTradeProfits.Add(profit);
while (_closedTradeProfits.Count > 5)
_closedTradeProfits.RemoveAt(0);
}
private void ResetEntryState()
{
_entrySide = null;
_entryVolume = 0m;
_entryPrice = null;
_entryTime = null;
_trailingStopPrice = null;
}
private void InitializePipSize()
{
var security = Security;
if (security == null)
{
_pipSize = 0m;
return;
}
var step = security.PriceStep ?? 0m;
if (step <= 0m)
step = 0.0001m;
if (security.Decimals is int decimals && (decimals == 3 || decimals == 5))
_pipSize = step * 10m;
else
_pipSize = step;
}
private decimal EnsurePipSize()
{
if (_pipSize <= 0m)
InitializePipSize();
return _pipSize;
}
private decimal GetTrailingDistance()
{
var pip = EnsurePipSize();
return pip > 0m ? TrailingStopPips * pip : 0m;
}
private static decimal RoundToOneDecimal(decimal value)
{
return Math.Round(value, 1, MidpointRounding.AwayFromZero);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class ten_pips_opposite_last_n_hour_trend_strategy(Strategy):
def __init__(self):
super(ten_pips_opposite_last_n_hour_trend_strategy, self).__init__()
self._fixed_volume = self.Param("FixedVolume", 0.1) \
.SetDisplay("Fixed Volume", "Fixed volume for entries", "Risk")
self._minimum_volume = self.Param("MinimumVolume", 0.1) \
.SetDisplay("Minimum Volume", "Minimum allowed volume", "Risk")
self._maximum_volume = self.Param("MaximumVolume", 5.0) \
.SetDisplay("Maximum Volume", "Maximum allowed volume", "Risk")
self._maximum_risk = self.Param("MaximumRisk", 0.05) \
.SetDisplay("Maximum Risk", "Risk fraction when Fixed Volume is zero", "Risk")
self._trading_hour = self.Param("TradingHour", 7) \
.SetDisplay("Trading Hour", "Hour when entries are allowed", "Trading")
self._hours_to_check_trend = self.Param("HoursToCheckTrend", 30) \
.SetDisplay("Hours To Check Trend", "Look-back hours for trend detection", "Trading")
self._stop_loss_pips = self.Param("StopLossPips", 50.0) \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 10.0) \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk")
self._first_multiplier = self.Param("FirstMultiplier", 4.0) \
.SetDisplay("First Multiplier", "Multiplier after the last loss", "Money Management")
self._second_multiplier = self.Param("SecondMultiplier", 2.0) \
.SetDisplay("Second Multiplier", "Multiplier if only the previous trade lost", "Money Management")
self._third_multiplier = self.Param("ThirdMultiplier", 5.0) \
.SetDisplay("Third Multiplier", "Multiplier if only the third trade lost", "Money Management")
self._fourth_multiplier = self.Param("FourthMultiplier", 5.0) \
.SetDisplay("Fourth Multiplier", "Multiplier if only the fourth trade lost", "Money Management")
self._fifth_multiplier = self.Param("FifthMultiplier", 1.0) \
.SetDisplay("Fifth Multiplier", "Multiplier if only the fifth trade lost", "Money Management")
self._order_max_age_seconds = self.Param("OrderMaxAgeSeconds", 75600) \
.SetDisplay("Max Position Age (s)", "Maximum holding time in seconds", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle type used for analysis", "Trading")
self._close_history = []
self._closed_trade_profits = []
self._pip_size = 0.0
self._last_bar_traded = None
self._entry_side = None
self._entry_volume = 0.0
self._entry_price = None
self._entry_time = None
self._trailing_stop_price = None
@property
def FixedVolume(self):
return self._fixed_volume.Value
@property
def MinimumVolume(self):
return self._minimum_volume.Value
@property
def MaximumVolume(self):
return self._maximum_volume.Value
@property
def MaximumRisk(self):
return self._maximum_risk.Value
@property
def TradingHour(self):
return self._trading_hour.Value
@property
def HoursToCheckTrend(self):
return self._hours_to_check_trend.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def FirstMultiplier(self):
return self._first_multiplier.Value
@property
def SecondMultiplier(self):
return self._second_multiplier.Value
@property
def ThirdMultiplier(self):
return self._third_multiplier.Value
@property
def FourthMultiplier(self):
return self._fourth_multiplier.Value
@property
def FifthMultiplier(self):
return self._fifth_multiplier.Value
@property
def OrderMaxAgeSeconds(self):
return self._order_max_age_seconds.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ten_pips_opposite_last_n_hour_trend_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._update_close_history(float(candle.ClosePrice))
if self.Position != 0 and self._update_protective_logic(candle):
return
if self.Position != 0 and self._close_expired_position(candle.CloseTime):
return
if not self._is_trading_hour(candle.CloseTime):
self._flatten()
return
if not self._has_trend_sample():
return
if not self._can_open_on_bar(candle.OpenTime):
return
if self.Position != 0:
return
direction = self._determine_direction()
if direction == 0:
return
volume = self._calculate_order_volume(float(candle.ClosePrice))
if volume <= 0:
return
if direction > 0:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
self._last_bar_traded = candle.OpenTime
def _update_protective_logic(self, candle):
if self._entry_side is None or self._entry_price is None or self._entry_volume <= 0:
return False
pip = self._ensure_pip_size()
if pip <= 0:
return False
sl_dist = float(self.StopLossPips) * pip
tp_dist = float(self.TakeProfitPips) * pip
trail_dist = float(self.TrailingStopPips) * pip
high_price = float(candle.HighPrice)
low_price = float(candle.LowPrice)
entry = self._entry_price
if self._entry_side == Sides.Buy:
if float(self.StopLossPips) > 0 and low_price <= entry - sl_dist:
self.SellMarket(Math.Abs(self.Position))
return True
if float(self.TakeProfitPips) > 0 and high_price >= entry + tp_dist:
self.SellMarket(Math.Abs(self.Position))
return True
if float(self.TrailingStopPips) > 0 and trail_dist > 0:
candidate = high_price - trail_dist
if high_price - entry > trail_dist:
if self._trailing_stop_price is None or candidate > self._trailing_stop_price:
self._trailing_stop_price = candidate
if self._trailing_stop_price is not None and low_price <= self._trailing_stop_price:
self.SellMarket(Math.Abs(self.Position))
return True
elif self._entry_side == Sides.Sell:
if float(self.StopLossPips) > 0 and high_price >= entry + sl_dist:
self.BuyMarket(Math.Abs(self.Position))
return True
if float(self.TakeProfitPips) > 0 and low_price <= entry - tp_dist:
self.BuyMarket(Math.Abs(self.Position))
return True
if float(self.TrailingStopPips) > 0 and trail_dist > 0:
candidate = low_price + trail_dist
if self._trailing_stop_price is None or candidate < self._trailing_stop_price:
self._trailing_stop_price = candidate
if self._trailing_stop_price is not None and high_price >= self._trailing_stop_price:
self.BuyMarket(Math.Abs(self.Position))
return True
return False
def _close_expired_position(self, time):
max_age = self.OrderMaxAgeSeconds
if max_age <= 0 or self._entry_time is None:
return False
age = time - self._entry_time
if age.TotalSeconds < max_age:
return False
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
return True
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
return True
return False
def _is_trading_hour(self, time):
hour = time.Hour
return hour == self.TradingHour
def _can_open_on_bar(self, bar_open_time):
if self._last_bar_traded is not None and self._last_bar_traded == bar_open_time:
return False
return True
def _flatten(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
def _has_trend_sample(self):
return self.HoursToCheckTrend > 0 and len(self._close_history) >= self.HoursToCheckTrend
def _determine_direction(self):
if len(self._close_history) == 0:
return 0
recent_close = self._close_history[-1]
older_index = len(self._close_history) - self.HoursToCheckTrend
if older_index < 0 or older_index >= len(self._close_history):
return 0
older_close = self._close_history[older_index]
return 1 if older_close > recent_close else -1
def _calculate_order_volume(self, price):
fv = float(self.FixedVolume)
if fv > 0:
base_volume = fv
else:
equity = 0.0
if self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
equity = float(self.Portfolio.CurrentValue)
max_risk = float(self.MaximumRisk)
if equity > 0 and max_risk > 0:
base_volume = round(equity * max_risk / 1000.0, 1)
else:
base_volume = float(self.Volume) if self.Volume > 0 else 1.0
base_volume = self._apply_loss_multipliers(base_volume)
min_vol = float(self.MinimumVolume)
max_vol = float(self.MaximumVolume)
if base_volume < min_vol:
base_volume = min_vol
elif base_volume > max_vol:
base_volume = max_vol
return base_volume
def _apply_loss_multipliers(self, volume):
if len(self._closed_trade_profits) == 0:
return volume
multipliers = [
float(self.FirstMultiplier),
float(self.SecondMultiplier),
float(self.ThirdMultiplier),
float(self.FourthMultiplier),
float(self.FifthMultiplier),
]
count = len(self._closed_trade_profits)
for i in range(min(len(multipliers), count)):
profit = self._closed_trade_profits[count - 1 - i]
if profit < 0:
volume *= multipliers[i]
break
if profit > 0:
break
return volume
def _update_close_history(self, close):
if close <= 0:
return
self._close_history.append(close)
max_len = max(self.HoursToCheckTrend + 2, 64)
while len(self._close_history) > max_len:
self._close_history.pop(0)
def _add_closed_trade_profit(self, profit):
self._closed_trade_profits.append(profit)
while len(self._closed_trade_profits) > 5:
self._closed_trade_profits.pop(0)
def _calculate_pip_size(self):
if self.Security is None:
return 0.0001
step = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 0.0
if step <= 0:
step = 0.0001
return step
def _ensure_pip_size(self):
if self._pip_size <= 0:
self._pip_size = self._calculate_pip_size()
return self._pip_size
def _reset_entry_state(self):
self._entry_side = None
self._entry_volume = 0.0
self._entry_price = None
self._entry_time = None
self._trailing_stop_price = None
def OnOwnTradeReceived(self, trade):
super(ten_pips_opposite_last_n_hour_trend_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None or trade.Trade is None:
return
price = float(trade.Trade.Price)
volume = float(trade.Trade.Volume)
time = trade.Trade.ServerTime
if volume <= 0 or price <= 0:
return
if self._entry_side is None or self._entry_side == trade.Order.Side:
total_volume = self._entry_volume + volume
if total_volume <= 0:
self._reset_entry_state()
return
if self._entry_volume > 0 and self._entry_price is not None:
self._entry_price = (self._entry_price * self._entry_volume + price * volume) / total_volume
else:
self._entry_price = price
self._entry_volume = total_volume
self._entry_side = trade.Order.Side
if self._entry_time is None:
self._entry_time = time
else:
if self._entry_side is None or self._entry_price is None or self._entry_volume <= 0:
return
remaining = self._entry_volume - volume
if remaining < 0:
remaining = 0
profit = 0.0
if self._entry_side == Sides.Buy:
profit = (price - self._entry_price) * volume
elif self._entry_side == Sides.Sell:
profit = (self._entry_price - price) * volume
self._add_closed_trade_profit(profit)
if remaining == 0:
self._reset_entry_state()
else:
self._entry_volume = remaining
self._entry_time = time
def OnReseted(self):
super(ten_pips_opposite_last_n_hour_trend_strategy, self).OnReseted()
self._close_history = []
self._closed_trade_profits = []
self._last_bar_traded = None
self._entry_side = None
self._entry_volume = 0.0
self._entry_price = None
self._entry_time = None
self._trailing_stop_price = None
self._pip_size = 0.0
def CreateClone(self):
return ten_pips_opposite_last_n_hour_trend_strategy()