在 GitHub 上查看

PLC 策略

概述

PLC 策略在 StockSharp 高级 API 中复刻了 MetaTrader 专家顾问 PLC (barabashkakvn's edition) 的运行方式。算法基于参数“Entry Timeframe”指定的主周期,在最近一根已完成 K 线的高点上方和低点下方分别挂入突破止损单。默认会订阅 M5 与 H1 两个周期的分形指标,以此动态放大下单手数。当持仓的浮动利润超过设定的阈值时,策略会清空持仓并重新等待下一个信号。

运行流程

  1. 新 K 线触发:只有当主周期的 K 线完全收盘后才会开始计算,避免信号重绘。
  2. 维护阶段:在评估新信号之前,策略会取消需要删除的止损单,并在上一根 K 线达到盈利目标时平掉仓位。
  3. 价格偏移:上一根 K 线的最高价和最低价会按照 Shift OHLC 参数向外偏移若干点(自动适配 3/5 位报价)。
  4. 分形更新:通过独立订阅追踪 M5 和 H1 周期的五根 K 线分形,记录最近一次上、下分形的价格。
  5. 距离判断:只有当新的买入止损价距离当前所有多单的最高开仓价至少 Shift Position 点,或当前没有多单且没有挂单时,才会挂出新的买入止损。卖出条件与之相反。
  6. 手数调整:当触发价突破对应周期的分形时,基础手数(Buy Volume/Sell Volume)会乘以 M5 或 H1 的放大系数;系数设为 0 可关闭该周期的加仓功能。
  7. 下单管理:通过 BuyStop/SellStop 发送止损单,并保存返回的订单对象,便于后续取消。
  8. 盈利目标:依据品种的 PriceStepStepPrice 计算持仓浮盈,当盈利超过 Minimum Profit 时设置关闭标志,在下一次循环用市价单平仓。
  9. 成交反馈:一旦某个止损单成交,其余止损单都会被取消,以与原始 MQL 逻辑保持一致。

参数

参数 说明
Shift OHLC 相对于上一根 K 线高低点的偏移量(单位:点)。
Minimum Profit 触发全部平仓的盈利阈值。
Shift Position 新挂止损价与当前持仓开仓价之间的最小间距,防止密集加仓。
Buy Volume / Sell Volume 未应用分形乘数之前的基础下单手数。
M5 Multiplier / H1 Multiplier 当止损价突破对应周期分形时的手数乘数,0 表示关闭。
Entry Timeframe 产生信号的主周期。
M5 Fractal Timeframe 计算低周期分形所使用的周期(默认 5 分钟)。
H1 Fractal Timeframe 计算高周期分形所使用的周期(默认 1 小时)。

仓位控制

  • 止损单管理:策略保存所有挂出的止损单引用,当任一止损单成交后,其余止损单会在下一次检查时被取消。
  • 盈利平仓:盈利达到阈值时,使用市价单平掉净头寸(多头用 SellMarket,空头用 BuyMarket),仓位归零后重置关闭标志。
  • 持仓跟踪:每笔成交会记录为独立的仓位单元,用于还原最高买入价与最低卖出价,保持与 MetaTrader 相同的统计方式。

注意事项

  • 默认参数按照原始 EA 设置,可以根据品种特性调整主周期和分形周期。
  • 下单量会根据交易所的数量步长向下取整,如果结果为 0 则跳过下单。
  • 盈利计算使用 PriceStepStepPrice,适用于 tick 价值不为 1 的品种。
namespace StockSharp.Samples.Strategies;

using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// PLC (Price Level Channel) strategy.
/// Buys when price breaks above the previous candle's high plus an offset,
/// sells when price breaks below the previous candle's low minus an offset.
/// Uses ATR to dynamically adjust the offset.
/// </summary>
public class PlcStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _shiftPips;
	private readonly StrategyParam<int> _atrPeriod;

	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _initialized;

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

	public int ShiftPips
	{
		get => _shiftPips.Value;
		set => _shiftPips.Value = value;
	}

	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	public PlcStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for calculations", "General");

		_shiftPips = Param(nameof(ShiftPips), 15)
			.SetGreaterThanZero()
			.SetDisplay("Shift Pips", "Offset added to candle high/low for breakout", "Trading");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period for volatility measurement", "Indicators");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevHigh = 0m;
		_prevLow = 0m;
		_initialized = false;
	}

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

		_prevHigh = 0;
		_prevLow = 0;
		_initialized = false;

		var atr = new AverageTrueRange { Length = AtrPeriod };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(atr, OnProcess)
			.Start();

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

	private void OnProcess(ICandleMessage candle, decimal atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!_initialized)
		{
			_prevHigh = candle.HighPrice;
			_prevLow = candle.LowPrice;
			_initialized = true;
			return;
		}

		var shift = atrValue * ShiftPips / 100m;
		var buyLevel = _prevHigh + shift;
		var sellLevel = _prevLow - shift;

		// Buy breakout: close above previous high + shift
		if (candle.ClosePrice > buyLevel && Position <= 0)
		{
			BuyMarket();
		}
		// Sell breakout: close below previous low - shift
		else if (candle.ClosePrice < sellLevel && Position >= 0)
		{
			SellMarket();
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
	}
}