Ten Pips Opposite Last N Hour Trend Strategy
Overview
This strategy is a faithful port of the MetaTrader expert 10pipsOnceADayOppositeLastNHourTrend. It trades exactly once per day at a configurable hour and deliberately takes the opposite side of the price change observed over the last N completed hourly candles. The logic is designed for currency pairs with five-digit pricing, but the C# version automatically adapts the pip size using the instrument's PriceStep and number of decimals.
At the selected trading hour the strategy inspects the closing price from HoursToCheckTrend hours ago and compares it with the close of the most recent completed hourly candle:
- If the older close is higher, the market has been falling (bearish), so the strategy opens a long position.
- Otherwise the market has been rising (bullish), therefore it opens a short position.
Positions are closed by protective stops, a daily time-based exit, or manually when the market is outside the trading window.
Money management
Position sizing mirrors the original expert's martingale ladder:
- The base volume comes from
FixedVolume. When set to zero the strategy falls back to risk-based sizing usingPortfolio.CurrentValue * MaximumRisk / 1000rounded to one decimal place. - The volume is limited by
MinimumVolume,MaximumVolume, the instrument's volume limits, and a soft cap equal toPortfolio.CurrentValue / 1000lots. - After each closed trade the result is stored (up to the last five trades). When preparing a new entry the strategy scans that history and multiplies the lot size according to the first loss it finds, using the
FirstMultiplier…FifthMultipliersequence. This reproduces the nestedOrderSelectchecks from the MQL version.
Risk controls
StopLossPips,TakeProfitPips, andTrailingStopPipswork in pip units. The port recalculates the pip size with the standard 3/5-decimal multiplier for Forex symbols.- Trailing stops are symmetric for long and short positions. In the original MQL code the short-side trail never triggered because of a sign error; the C# version fixes that so both directions behave identically.
OrderMaxAgecloses any position that survives longer than the configured duration (21 hours by default).- Outside of the allowed trading hour the strategy liquidates any open exposure to stay flat until the next session.
MaxOrdersguards against accidental re-entries by requiring that there are no open positions or active orders when a new signal is evaluated.
Detailed workflow
- Subscribe to hourly candles (the timeframe can be changed with
CandleType). - Collect the close price of each finished candle in a small rolling buffer.
- On the first completed candle at the allowed hour:
- Check the portfolio/connection state and confirm no position is open.
- Ensure we have at least
HoursToCheckTrendhistorical candles to compare. - Determine the direction by comparing the current close with the close
HoursToCheckTrendbars ago. - Compute the lot size using the money-management routine above and send a market order.
- While a position is open the strategy:
- Evaluates stop-loss, take-profit, and trailing levels using candle high/low prices.
- Updates the trailing stop after new highs (for longs) or lows (for shorts).
- Tracks the entry timestamp so it can enforce
OrderMaxAge. - Records the realized profit/loss when the trade closes to feed the martingale multipliers.
Parameters
| Parameter | Description | Default |
|---|---|---|
FixedVolume |
Fixed lot size. Set to 0 to use risk-based sizing. |
0.1 |
MinimumVolume |
Hard lower bound for the order volume. | 0.1 |
MaximumVolume |
Hard upper bound for the order volume. | 5 |
MaximumRisk |
Fraction of equity used when FixedVolume = 0. |
0.05 |
MaxOrders |
Maximum simultaneous orders/positions. | 1 |
TradingHour |
Hour of day (0–23) when new trades are allowed. | 7 |
HoursToCheckTrend |
Look-back window in hours for the trend comparison. | 30 |
OrderMaxAge |
Maximum lifetime of a position. | 21h |
StopLossPips |
Stop-loss distance in pips. | 50 |
TakeProfitPips |
Take-profit distance in pips. | 10 |
TrailingStopPips |
Trailing-stop distance in pips. | 0 (disabled) |
FirstMultiplier … FifthMultiplier |
Lot multipliers applied when the most recent losing trade is found at the respective depth. | 4, 2, 5, 5, 1 |
CandleType |
Time frame for candle subscription. | 1 hour |
Differences from the original MQL expert
- Martingale sizing, order-aging, and trading window logic are reproduced one-to-one. The only deliberate change is the symmetrical short-side trailing stop to correct the sign bug in the original script.
- All protective levels are executed with market orders on the next finished candle because StockSharp strategies do not register separate stop/limit orders when using high-level helpers. This matches the behaviour of the original expert when its stop orders were triggered.
- Account equity is read from
Portfolio.CurrentValue. If the adapter does not provide this field the strategy falls back to the baseVolume(default1). - The list of allowed trading hours mirrors the original array of
0…23. To restrict trading to specific days you can edit_tradingDayHoursinside the constructor.
Usage notes
- Works best on hourly Forex data where pip size calculations using the
PriceStep×10 heuristic are valid. - Always verify that
Security.VolumeStep,VolumeMin, andVolumeMaxare set by the connector so the strategy can adjust lot sizes correctly. - Because entries are evaluated only once per finished candle, the strategy should be launched before the chosen trading hour so the first signal of the day is not missed.
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()