在 GitHub 上查看

Total Power Indicator X 策略

概览

该策略使用 StockSharp 的高级 API 重现 MetaTrader 专家顾问 “Exp_TotalPowerIndicatorX”。自定义的 Total Power 指标通过统计滚动窗口内多少根 K 线的高点高于内部 EMA、低点低于内部 EMA 来衡量多空力量,并在两条力量线发生交叉时做出交易决策。

指标适用于任意品种和周期。默认订阅 4 小时 K 线,以匹配原始 EA 的设置,不过可以通过参数自由调整时间框架。

交易逻辑

  1. 每当一根 K 线收盘时,将数据送入 Total Power 指标,该指标会:
    • Power Period 指定的周期计算 EMA。
    • 在最近 Lookback Period 根 K 线中统计 High > EMA 的次数(多头)以及 Low < EMA 的次数(空头)。
    • 将计数转换为 0–100 范围的百分比型力量值。
  2. 当多头力量上穿空头力量时,如果允许做多且当前没有持仓,则开多。
  3. 当空头力量上穿多头力量时,如果允许做空且当前没有持仓,则开空。
  4. 如果开启了对应的平仓开关,则在出现相反交叉时平掉已有头寸。
  5. 可选的交易时段过滤器会在离开设定时间窗口时强制平仓,并禁止在该时间段外开仓。起始时间晚于结束时间的跨夜窗口同样支持。
  6. 可选的止损与止盈以价格步长的倍数表示。当 K 线的最高价或最低价触及这些水平时,会立即发出平仓指令并重新计算目标。

参数

  • Candle Type:用于计算的时间框架,默认 4 小时。
  • Power Period:指标内部 EMA 的长度,对应 MQL 输入参数,默认 10。
  • Lookback:多空力量统计所使用的 K 线数量,默认 45。
  • Volume:每次下单的数量,默认 1。
  • Enable Long Entry / Enable Short Entry:允许或禁止新的多头/空头建仓。
  • Enable Long Exit / Enable Short Exit:在相反信号出现时是否自动平仓。关闭后头寸会保持,直到手动或被止损止盈处理。
  • Use Trading Hours:启用时段过滤,仅在 Start Hour/MinuteEnd Hour/Minute 之间进行交易,并在时段外平掉现有头寸。
  • Stop Loss Points / Take Profit Points:距离开仓价的价格步长数量。设置为 0 表示禁用,需要保证 Security.PriceStep 元数据可用。

说明

  • 策略只有在该品种没有持仓时才会开新仓,从而与原版 EA 的行为保持一致。
  • 止损与止盈依赖于品种的价格步长,如果元数据缺失则会自动禁用这些保护。
  • 若界面可用,策略会在图表面板上绘制 Total Power 指标,便于观察多空力量的交叉情况。
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 replicates the Total Power Indicator expert advisor.
/// </summary>
public class TotalPowerIndicatorXStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _powerPeriod;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<bool> _enableLongEntry;
	private readonly StrategyParam<bool> _enableShortEntry;
	private readonly StrategyParam<bool> _enableLongExit;
	private readonly StrategyParam<bool> _enableShortExit;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private TotalPowerIndicator _totalPower;
	private decimal? _previousDifference;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;

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

	/// <summary>
	/// Period for EMA inside Total Power Indicator.
	/// </summary>
	public int PowerPeriod
	{
		get => _powerPeriod.Value;
		set => _powerPeriod.Value = value;
	}

	/// <summary>
	/// Lookback period used for bull and bear strength counters.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}


	/// <summary>
	/// Enable opening long positions.
	/// </summary>
	public bool EnableLongEntry
	{
		get => _enableLongEntry.Value;
		set => _enableLongEntry.Value = value;
	}

	/// <summary>
	/// Enable opening short positions.
	/// </summary>
	public bool EnableShortEntry
	{
		get => _enableShortEntry.Value;
		set => _enableShortEntry.Value = value;
	}

	/// <summary>
	/// Enable closing long positions on opposite signals.
	/// </summary>
	public bool EnableLongExit
	{
		get => _enableLongExit.Value;
		set => _enableLongExit.Value = value;
	}

	/// <summary>
	/// Enable closing short positions on opposite signals.
	/// </summary>
	public bool EnableShortExit
	{
		get => _enableShortExit.Value;
		set => _enableShortExit.Value = value;
	}

	/// <summary>
	/// Enable time filter for trading sessions.
	/// </summary>
	public bool UseTradingHours
	{
		get => _useTradingHours.Value;
		set => _useTradingHours.Value = value;
	}

	/// <summary>
	/// Session start hour.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Session start minute.
	/// </summary>
	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	/// <summary>
	/// Session end hour.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Session end minute.
	/// </summary>
	public int EndMinute
	{
		get => _endMinute.Value;
		set => _endMinute.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="TotalPowerIndicatorXStrategy"/>.
	/// </summary>
	public TotalPowerIndicatorXStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for calculations", "General");

		_powerPeriod = Param(nameof(PowerPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Power Period", "EMA length used by Total Power", "Indicator")
		;

		_lookbackPeriod = Param(nameof(LookbackPeriod), 50)
		.SetGreaterThanZero()
		.SetDisplay("Lookback", "Samples counted for bull/bear strength", "Indicator")
		;


		_enableLongEntry = Param(nameof(EnableLongEntry), true)
		.SetDisplay("Enable Long Entry", "Allow buying when bulls dominate", "Trading");

		_enableShortEntry = Param(nameof(EnableShortEntry), true)
		.SetDisplay("Enable Short Entry", "Allow selling when bears dominate", "Trading");

		_enableLongExit = Param(nameof(EnableLongExit), true)
		.SetDisplay("Enable Long Exit", "Close longs on bearish crossover", "Trading");

		_enableShortExit = Param(nameof(EnableShortExit), true)
		.SetDisplay("Enable Short Exit", "Close shorts on bullish crossover", "Trading");

		_useTradingHours = Param(nameof(UseTradingHours), false)
		.SetDisplay("Use Trading Hours", "Restrict trading to session window", "Schedule");

		_startHour = Param(nameof(StartHour), 0)
		.SetDisplay("Start Hour", "Session start hour", "Schedule");

		_startMinute = Param(nameof(StartMinute), 0)
		.SetDisplay("Start Minute", "Session start minute", "Schedule");

		_endHour = Param(nameof(EndHour), 23)
		.SetDisplay("End Hour", "Session end hour", "Schedule");

		_endMinute = Param(nameof(EndMinute), 59)
		.SetDisplay("End Minute", "Session end minute", "Schedule");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
		.SetDisplay("Stop Loss Points", "Stop loss distance in price steps (0=disabled)", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
		.SetDisplay("Take Profit Points", "Take profit distance in price steps (0=disabled)", "Risk");
	}

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

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

		_totalPower?.Reset();
		_previousDifference = null;
		ResetLongTargets();
		ResetShortTargets();
	}

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

		_totalPower = new TotalPowerIndicator
		{
			PowerPeriod = PowerPeriod,
			LookbackPeriod = LookbackPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_totalPower, ProcessCandle)
		.Start();

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

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

		if (indicatorValue is not TotalPowerIndicatorValue powerValue)
		return;

		if (!_totalPower.IsFormed)
		{
			_previousDifference = powerValue.Bulls - powerValue.Bears;
			return;
		}

		var difference = powerValue.Bulls - powerValue.Bears;
		var previous = _previousDifference ?? difference;
		_previousDifference = difference;

		if (HandleStops(candle))
		return;

		var crossUp = difference > 0m && previous <= 0m;
		var crossDown = difference < 0m && previous >= 0m;

		var isTradingTime = !UseTradingHours || IsWithinTradingWindow(candle.OpenTime);

		if (UseTradingHours && !isTradingTime)
		{
			CloseAllPositions();
			return;
		}

		if (EnableLongExit && crossDown && Position > 0m)
		{
			SellMarket();
			ResetLongTargets();
		}

		if (EnableShortExit && crossUp && Position < 0m)
		{
			BuyMarket();
			ResetShortTargets();
		}

		if (!isTradingTime)
		return;

		if (EnableLongEntry && crossUp && Position == 0m)
		{
			BuyMarket();
			SetupLongTargets(candle.ClosePrice);
		}
		else if (EnableShortEntry && crossDown && Position == 0m)
		{
			SellMarket();
			SetupShortTargets(candle.ClosePrice);
		}
	}

	private bool HandleStops(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		return false;

		if (Position > 0m)
		{
			if (_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongTargets();
				return true;
			}

			if (_longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongTargets();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortTargets();
				return true;
			}

			if (_shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortTargets();
				return true;
			}
		}

		return false;
	}

	private bool IsWithinTradingWindow(DateTimeOffset time)
	{
		var start = new TimeSpan(StartHour, StartMinute, 0);
		var end = new TimeSpan(EndHour, EndMinute, 0);
		var current = time.TimeOfDay;

		if (start == end)
			return true;

		if (start < end)
			return current >= start && current < end;

		return current >= start || current < end;
	}

	private void CloseAllPositions()
	{
		if (Position > 0m)
		{
			SellMarket();
			ResetLongTargets();
		}
		else if (Position < 0m)
		{
			BuyMarket();
			ResetShortTargets();
		}
	}

	private void SetupLongTargets(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		{
			ResetLongTargets();
			return;
		}

		_longStopPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * step : null;
		_longTakePrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * step : null;
		ResetShortTargets();
	}

	private void SetupShortTargets(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		{
			ResetShortTargets();
			return;
		}

		_shortStopPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * step : null;
		_shortTakePrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * step : null;
		ResetLongTargets();
	}

	private void ResetLongTargets()
	{
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortTargets()
	{
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

	private sealed class TotalPowerIndicator : BaseIndicator
	{
		private readonly List<int> _bullHistory = new();
		private readonly List<int> _bearHistory = new();
		private readonly ExponentialMovingAverage _ema = new();
		private int _bullCount;
		private int _bearCount;
		private int _powerPeriod = 10;
		private int _lookbackPeriod = 50;

		public int PowerPeriod
		{
			get => _powerPeriod;
			set
			{
				_powerPeriod = Math.Max(1, value);
				_ema.Length = _powerPeriod;
			}
		}

		public int LookbackPeriod
		{
			get => _lookbackPeriod;
			set => _lookbackPeriod = Math.Max(1, value);
		}

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.ClosePrice, input.Time) { IsFinal = input.IsFinal });

			if (!_ema.IsFormed)
			{
				IsFormed = false;
				return new TotalPowerIndicatorValue(this, input, 0m, 0m, 0m);
			}

			var ema = emaValue.ToDecimal();
			var bullContribution = candle.HighPrice > ema ? 1 : 0;
			var bearContribution = candle.LowPrice < ema ? 1 : 0;

			UpdateCounters(_bullHistory, ref _bullCount, bullContribution);
			UpdateCounters(_bearHistory, ref _bearCount, bearContribution);

			if (_bullHistory.Count < LookbackPeriod || _bearHistory.Count < LookbackPeriod)
			{
				IsFormed = false;
				return new TotalPowerIndicatorValue(this, input, 0m, 0m, 0m);
			}

			var bullPercent = (decimal)_bullCount * 100m / LookbackPeriod;
			var bearPercent = (decimal)_bearCount * 100m / LookbackPeriod;

			var bulls = Math.Clamp((bullPercent - 50m) * 2m, 0m, 100m);
			var bears = Math.Clamp((bearPercent - 50m) * 2m, 0m, 100m);
			var power = Math.Clamp(2m * Math.Abs(bullPercent - bearPercent), 0m, 100m);

			IsFormed = true;
			return new TotalPowerIndicatorValue(this, input, bulls, bears, power);
		}

		public override void Reset()
		{
			base.Reset();

			_bullHistory.Clear();
			_bearHistory.Clear();
			_bullCount = 0;
			_bearCount = 0;
			_ema.Reset();
		}

		private void UpdateCounters(List<int> list, ref int count, int value)
		{
			list.Add(value);
			count += value;

			while (list.Count > LookbackPeriod)
			{
				try { count -= list[0]; list.RemoveAt(0); }
				catch { break; }
			}
		}
	}

	private sealed class TotalPowerIndicatorValue : DecimalIndicatorValue
	{
		public TotalPowerIndicatorValue(IIndicator indicator, IIndicatorValue input, decimal bulls, decimal bears, decimal power)
		: base(indicator, bulls - bears, input.Time)
		{
			Bulls = bulls;
			Bears = bears;
			Power = power;
		}

		public decimal Bulls { get; }

		public decimal Bears { get; }

		public decimal Power { get; }
	}
}