在 GitHub 上查看

20 Pips Opposite Last N Hour Trend 策略

该策略是 MetaTrader 顾问 “20 Pips Opposite Last N Hour Trend” 的 StockSharp 高阶移植版本。系统在每根小时 K 线收盘后记录价格,并在设定交易小时结束 时比较 N 小时前的收盘价与上一根 K 线,随后在相反方向开仓。仓位带有 固定 20 点(pip)的止盈和一小时超时退出,同时使用分级放大的马丁格尔式 资金管理来应对连续亏损。

交易逻辑

  • 订阅指定的蜡烛类型(默认 1 小时)。
  • 每根已结束的蜡烛都会把收盘价写入内部历史列表。
  • OpenTime.Hour == TradingHour 的蜡烛收盘且历史数据充足时:
    • HoursToCheckTrend 根之前的收盘价与上一根收盘价比较。
    • 若价格在此区间下跌,则在当前收盘后买入;若上涨,则卖出;若持平则 不交易。
  • 每个交易日只在指定小时开仓一次,其余蜡烛仅用于仓位管理。

仓位管理

  • 开仓后立即计算 20 点的止盈(会根据 3/5 位报价自动调整)。若任一已完 成蜡烛的最高价/最低价触及目标,则按该价格平仓。
  • 若目标未命中,则在下一根蜡烛收盘时按市价退出,避免隔夜暴露。
  • 新交易日开始时自动重置计数,允许下一次信号触发。

资金管理

  • Volume 为基础下单量,MaxVolume 限制放大后的最大手数。
  • 连续亏损时按阶梯放大手数:第一次亏损→FirstMultiplier,第二次→ SecondMultiplier,依此类推,超过五次依旧使用第五级倍数。一旦盈利或 打平即重置倍数序列。
  • 盈亏判定基于记录的开仓价与退出价,无需依赖外部 PnL 数据。

参数

参数 默认值 说明
MaxPositions 9 每日最多交易次数,设为 0 将关闭策略。
Volume 0.1 序列第一笔交易的基础手数。
MaxVolume 5 应用倍数后的手数上限。
TakeProfitPips 20 止盈距离(点数),0 表示不设止盈。
TradingHour 7 允许开仓的小时(0-23)。
HoursToCheckTrend 24 用于衡量趋势的历史小时数。
FirstMultiplier 2 第一次连续亏损后的倍数。
SecondMultiplier 4 第二次连续亏损后的倍数。
ThirdMultiplier 8 第三次连续亏损后的倍数。
FourthMultiplier 16 第四次连续亏损后的倍数。
FifthMultiplier 32 第五次及之后连续亏损的倍数。
CandleType H1 用于信号与仓位管理的蜡烛类型。

其他说明

  • 通过 Security.PriceStep 与小数位数估算每个点值,从而适配 4 位与 5 位 报价的外汇品种。
  • 启动时调用 StartProtection(),以启用 StockSharp 的保护机制。
  • 策略仅使用已完成的蜡烛,完全符合仓库 AGENTS.md 中的约束。

风险提示: 马丁格尔加仓可能导致极大的回撤。请务必在历史数据上 充分测试,并设置合理的风险控制后再用于实盘。

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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that trades against the last N hours trend with a fixed take profit.
/// </summary>
public class TwentyPipsOppositeLastNHourTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<int> _tradingHour;
	private readonly StrategyParam<int> _hoursToCheckTrend;
	private readonly StrategyParam<int> _firstMultiplier;
	private readonly StrategyParam<int> _secondMultiplier;
	private readonly StrategyParam<int> _thirdMultiplier;
	private readonly StrategyParam<int> _fourthMultiplier;
	private readonly StrategyParam<int> _fifthMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();

	private decimal? _entryPrice;
	private decimal? _takeProfitLevel;
	private decimal _entryVolume;
	private int _positionDirection;
	private int _consecutiveLosses;
	private DateTime? _currentDay;
	private int _tradesToday;

	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}


	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int TradingHour
	{
		get => _tradingHour.Value;
		set => _tradingHour.Value = value;
	}

	public int HoursToCheckTrend
	{
		get => _hoursToCheckTrend.Value;
		set => _hoursToCheckTrend.Value = value;
	}

	public int FirstMultiplier
	{
		get => _firstMultiplier.Value;
		set => _firstMultiplier.Value = value;
	}

	public int SecondMultiplier
	{
		get => _secondMultiplier.Value;
		set => _secondMultiplier.Value = value;
	}

	public int ThirdMultiplier
	{
		get => _thirdMultiplier.Value;
		set => _thirdMultiplier.Value = value;
	}

	public int FourthMultiplier
	{
		get => _fourthMultiplier.Value;
		set => _fourthMultiplier.Value = value;
	}

	public int FifthMultiplier
	{
		get => _fifthMultiplier.Value;
		set => _fifthMultiplier.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public TwentyPipsOppositeLastNHourTrendStrategy()
	{
		_maxPositions = Param(nameof(MaxPositions), 9)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum trades per day", "Trading");


		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Max Volume", "Maximum allowed volume", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading");

		_tradingHour = Param(nameof(TradingHour), 8)
			.SetRange(0, 23)
			.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing");

		_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 6)
			.SetRange(2, 240)
			.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals");

		_firstMultiplier = Param(nameof(FirstMultiplier), 2)
			.SetGreaterThanZero()
			.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management");

		_secondMultiplier = Param(nameof(SecondMultiplier), 4)
			.SetGreaterThanZero()
			.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management");

		_thirdMultiplier = Param(nameof(ThirdMultiplier), 8)
			.SetGreaterThanZero()
			.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management");

		_fourthMultiplier = Param(nameof(FourthMultiplier), 16)
			.SetGreaterThanZero()
			.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management");

		_fifthMultiplier = Param(nameof(FifthMultiplier), 32)
			.SetGreaterThanZero()
			.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_closeHistory.Clear();
		_entryPrice = null;
		_takeProfitLevel = null;
		_entryVolume = 0m;
		_positionDirection = 0;
		_consecutiveLosses = 0;
		_currentDay = null;
		_tradesToday = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		// no fixed protection needed
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		var candleDay = candle.OpenTime.Date;
		if (_currentDay != candleDay)
		{
			_currentDay = candleDay;
			_tradesToday = 0;
		}

		if (_positionDirection != 0)
		{
			if (_takeProfitLevel is decimal target)
			{
				// Take profit when the candle range touches the desired level.
				var hitTarget = _positionDirection > 0
					? candle.HighPrice >= target
					: candle.LowPrice <= target;

				if (hitTarget)
				{
					ClosePosition(target);
				}
			}

			if (_positionDirection != 0 && candle.OpenTime.Hour != TradingHour)
			{
				// Close remaining exposure when the configured session hour has passed.
				ClosePosition(candle.ClosePrice);
			}
		}

		if (_positionDirection != 0)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (candle.OpenTime.Hour != TradingHour)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (MaxPositions <= 0 || _tradesToday >= MaxPositions)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		var requiredHistory = Math.Max(HoursToCheckTrend, 2);
		if (_closeHistory.Count < requiredHistory)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		var referenceClose = _closeHistory[_closeHistory.Count - HoursToCheckTrend];
		var previousClose = _closeHistory[_closeHistory.Count - 1];

		if (previousClose == referenceClose)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		// Opposite trend logic: buy after bearish drift, sell after bullish drift.
		var goLong = previousClose < referenceClose;
		var orderVolume = CalculateOrderVolume();
		if (orderVolume <= 0)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (goLong)
		{
			Volume = orderVolume;
			BuyMarket();
			_positionDirection = 1;
		}
		else
		{
			Volume = orderVolume;
			SellMarket();
			_positionDirection = -1;
		}

		_entryPrice = candle.ClosePrice;
		_entryVolume = orderVolume;

		var distance = GetTakeProfitDistance();

		if (distance > 0m)
		{
			_takeProfitLevel = _positionDirection > 0
				? _entryPrice + distance
				: _entryPrice - distance;
		}
		else
		{
			_takeProfitLevel = null;
		}

		_tradesToday++;

		UpdateHistory(candle.ClosePrice);
	}

	private void ClosePosition(decimal exitPrice)
	{
		var direction = _positionDirection;
		var entryPrice = _entryPrice;
		var volume = Math.Abs(Position);

		if (volume <= 0m && _entryVolume > 0m)
		{
			volume = _entryVolume;
		}

		if (volume <= 0m)
		{
			_positionDirection = 0;
			_takeProfitLevel = null;
			_entryPrice = null;
			_entryVolume = 0m;
			return;
		}

		if (direction > 0)
		{
			SellMarket();
		}
		else if (direction < 0)
		{
			BuyMarket();
		}

		if (entryPrice is decimal price)
		{
			var isLoss = direction > 0
				? exitPrice < price
				: exitPrice > price;

			_consecutiveLosses = isLoss
				? Math.Min(_consecutiveLosses + 1, 5)
				: 0;
		}

		_positionDirection = 0;
		_takeProfitLevel = null;
		_entryPrice = null;
		_entryVolume = 0m;
	}

	private void UpdateHistory(decimal closePrice)
	{
		_closeHistory.Add(closePrice);

		var maxHistory = Math.Max(HoursToCheckTrend, 2);
		if (_closeHistory.Count > maxHistory)
		{
			_closeHistory.RemoveRange(0, _closeHistory.Count - maxHistory);
		}
	}

	private decimal CalculateOrderVolume()
	{
		if (Volume <= 0m)
		{
			return 0m;
		}

		var multiplier = _consecutiveLosses switch
		{
			>= 5 => (decimal)FifthMultiplier,
			4 => (decimal)FourthMultiplier,
			3 => (decimal)ThirdMultiplier,
			2 => (decimal)SecondMultiplier,
			1 => (decimal)FirstMultiplier,
			_ => 1m
		};

		var desiredVolume = Volume * multiplier;

		if (MaxVolume > 0m && desiredVolume > MaxVolume)
		{
			desiredVolume = MaxVolume;
		}

		return desiredVolume;
	}

	private decimal GetTakeProfitDistance()
	{
		var pipSize = GetPipSize();
		return pipSize > 0m
			? TakeProfitPips * pipSize
			: 0m;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
		{
			priceStep = 0.0001m;
		}

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
		{
			return priceStep * 10m;
		}

		return priceStep;
	}
}