Ten Pips Opposite Last N Hour Trend 策略
概述
该策略是 MetaTrader 专家 10pipsOnceADayOppositeLastNHourTrend 的移植版本。它每天只在一个指定的小时交易一次,并且刻意反向跟随最近 N 根小时 K 线的价格变化。原始脚本面向 5 位小数的外汇品种,C# 版本会根据 PriceStep 和小数位数自动换算点值,因此同样适用于 3 位小数的品种。
到达交易时间后,策略会比较 HoursToCheckTrend 小时之前的收盘价与最近一根已完成小时 K 线的收盘价:
- 如果较早的收盘价 更高,说明价格近段时间下跌,于是开出 多头 仓位。
- 否则价格上涨,则开出 空头 仓位。
仓位可以被保护性止损/止盈、持仓超时或超出交易时段等条件关闭。
资金管理
头寸规模完全复制原始 EA 的“阶梯式”马丁格尔逻辑:
- 基础手数来自
FixedVolume。当它为 0 时,按照Portfolio.CurrentValue * MaximumRisk / 1000计算,并四舍五入到 0.1 手。 - 结果会受到
MinimumVolume、MaximumVolume、交易所的最小/最大手数以及软限制Portfolio.CurrentValue / 1000的约束。 - 策略会保存最近五笔平仓结果。准备下一次进场时,按从近到远的顺序查找第一次出现的亏损,并使用
FirstMultiplier…FifthMultiplier中对应的倍数调整手数,完全模拟 MQL 中层层嵌套的OrderSelect判断。
风险控制
StopLossPips、TakeProfitPips、TrailingStopPips以点为单位。移植时按照外汇常用的 3/5 位小数规则自动放大 10 倍。- 多、空两侧的跟踪止损采用同一套逻辑。原始 EA 在空头方向存在符号错误导致永远不会移动止损,C# 版本修复了这一问题。
OrderMaxAge用于平掉持仓时间超过阈值(默认 21 小时)的订单。- 如果当前小时不在允许列表内,策略会立即平仓并等待下一次机会。
MaxOrders确保在有持仓或挂单时不会重复进场。
工作流程
- 订阅
CandleType指定的 K 线(默认 1 小时)。 - 将每根完成 K 线的收盘价写入滚动缓冲区。
- 在达到设定交易小时的第一根完成 K 线上:
- 检查连接状态并确认没有持仓。
- 确保历史缓冲区中至少包含
HoursToCheckTrend根 K 线。 - 比较当前收盘价与
HoursToCheckTrend小时前的收盘价,得出买卖方向。 - 根据资金管理规则计算手数并发送市价单。
- 持仓期间:
- 根据 K 线的最高价/最低价检查止损、止盈和跟踪止损是否触发。
- 创出新高/新低时,更新跟踪止损的位置。
- 记录建仓时间,用于判断
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 |
在最近第 1…5 笔亏损出现时的手数乘数。 | 4, 2, 5, 5, 1 |
CandleType |
计算所用的 K 线类型。 | 1 小时 |
与 MQL 版本的差异
- 马丁格尔、持仓时间和交易时间窗口等核心逻辑保持一致,唯一的改动是修复了空头方向的跟踪止损。
- 保护性止损/止盈在下一根完成 K 线上以市价平仓,这与原专家的实际效果一致。
- 账户权益读取自
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()