在 GitHub 上查看

Genie Pivot 策略

该策略实现了原始 MQL4 版本的 Genie Pivot 反转剥头皮思路。它等待由七根连续 K 线组成的转向形态,并使用固定止盈和跟踪止损来管理持仓。

策略逻辑

  1. 形态识别:当之前七个最低价持续下降且最新完成的 K 线形成更高的低点并收盘价高于前一根 K 线的最高价时产生做多信号;做空信号条件相反。
  2. 下单执行:信号确认后根据账户权益和风险参数计算下单量并以市价成交。
  3. 交易管理:开仓后设置止盈与跟踪止损。只有当利润超过跟踪距离时才激活跟踪止损。如果下一根 K 线反向(多单为阴线,空单为阳线)则立即平仓。
  4. 仓位递减:连续亏损会根据 Decrease Factor 参数降低交易量。

参数

名称 说明
TakeProfit 距离入场价的止盈距离(价格步长)。
TrailingStop 跟踪止损距离(价格步长)。
MaximumRisk 用于计算仓位的账户资金比例。
DecreaseFactor 连续亏损后减少仓位的系数。
BaseVolume 当账户价值未知时使用的基础数量。
CandleType 分析所用的 K 线周期。

备注

该策略仅处理收盘完成的 K 线。目前尚未提供 Python 版本。

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>
/// Pivot point reversal scalping strategy.
/// </summary>
public class GeniePivotStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private readonly decimal[] _lows = new decimal[8];
	private readonly decimal[] _highs = new decimal[8];
	private int _filled;

	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _targetPrice;
	private int _lossCount;
	private int _cooldownRemaining;

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Maximum risk used to calculate volume.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Factor to decrease volume after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Base volume used when account value is unknown.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Candle type for subscription.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of completed candles to wait after closing a position.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="GeniePivotStrategy"/>.
	/// </summary>
	public GeniePivotStrategy()
	{
		_takeProfit = Param(nameof(TakeProfit), 500m).SetDisplay("Take Profit", "Profit target in points", "Risk");

		_trailingStop =
			Param(nameof(TrailingStop), 200m).SetDisplay("Trailing Stop", "Trailing distance in points", "Risk");

		_maximumRisk = Param(nameof(MaximumRisk), 0.02m).SetDisplay("Maximum Risk", "Risk per trade", "Risk");

		_decreaseFactor =
			Param(nameof(DecreaseFactor), 3m).SetDisplay("Decrease Factor", "Volume reduction after losses", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m).SetDisplay("Base Volume", "Fallback volume", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
						  .SetDisplay("Candle Type", "Type of candles", "General");

		_cooldownBars = Param(nameof(CooldownBars), 4)
			.SetDisplay("Cooldown Bars", "Completed candles to wait after closing a position", "Risk");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_filled = 0;
		_entryPrice = default;
		_stopPrice = default;
		_targetPrice = default;
		_lossCount = 0;
		_cooldownRemaining = 0;

		Array.Clear(_lows);
		Array.Clear(_highs);
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private decimal GetVolume()
	{
		var lot = BaseVolume;

		if (MaximumRisk > 0m && Portfolio.CurrentValue is decimal value)
			lot = Math.Round(value * MaximumRisk / 1000m, 1);

		if (DecreaseFactor > 0m && _lossCount > 1)
			lot -= lot * _lossCount / DecreaseFactor;

		return lot < 0.1m ? 0.1m : lot;
	}

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var step = Security.PriceStep ?? 1m;
		var close = candle.ClosePrice;
		var open = candle.OpenPrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		// shift history
		for (var i = _lows.Length - 1; i > 0; i--)
		{
			_lows[i] = _lows[i - 1];
			_highs[i] = _highs[i - 1];
		}
		_lows[0] = low;
		_highs[0] = high;

		if (_filled < 4)
		{
			_filled++;
			return;
		}

		if (Position == 0)
		{
			if (_cooldownRemaining > 0)
				return;

			// Buy when 3 consecutive declining lows followed by a higher low (reversal)
			var buyCond = _lows[4] > _lows[3] && _lows[3] > _lows[2] && _lows[2] > _lows[1] &&
						  _lows[1] < _lows[0] && close > _highs[1] && close > open;

			// Sell when 3 consecutive rising highs followed by a lower high (reversal)
			var sellCond = _highs[4] < _highs[3] && _highs[3] < _highs[2] && _highs[2] < _highs[1] &&
						   _highs[1] > _highs[0] && close < _lows[1] && close < open;

			if (buyCond)
			{
				BuyMarket();
				_entryPrice = close;
				_stopPrice = _entryPrice - TrailingStop * step;
				_targetPrice = _entryPrice + TakeProfit * step;
			}
			else if (sellCond)
			{
				SellMarket();
				_entryPrice = close;
				_stopPrice = _entryPrice + TrailingStop * step;
				_targetPrice = _entryPrice - TakeProfit * step;
			}
		}
		else if (Position > 0)
		{
			if (close >= _targetPrice)
			{
				SellMarket();
				_lossCount = 0;
				_cooldownRemaining = CooldownBars;
			}
			else
			{
				if (close - _entryPrice > TrailingStop * step)
				{
					var newStop = close - TrailingStop * step;
					if (newStop > _stopPrice)
						_stopPrice = newStop;
				}

				if (low <= _stopPrice)
				{
					SellMarket();
					_lossCount++;
					_cooldownRemaining = CooldownBars;
				}
			}
		}
		else if (Position < 0)
		{
			if (close <= _targetPrice)
			{
				BuyMarket();
				_lossCount = 0;
				_cooldownRemaining = CooldownBars;
			}
			else
			{
				if (_entryPrice - close > TrailingStop * step)
				{
					var newStop = close + TrailingStop * step;
					if (newStop < _stopPrice)
						_stopPrice = newStop;
				}

				if (high >= _stopPrice)
				{
					BuyMarket();
					_lossCount++;
					_cooldownRemaining = CooldownBars;
				}
			}
		}
	}
}