在 GitHub 上查看

Prop Firm Helper 策略

概述

Prop Firm Helper 策略源自 MetaTrader 的 "Prop Firm Helper" 智能交易系统,使用唐奇安通道突破逻辑。策略在价格突破近期高点时挂买入止损单,在跌破近期低点时挂卖出止损单,并在达到挑战目标或触及每日亏损限制后自动停止交易。

交易逻辑

  • 根据 Candle Type 订阅指定周期的 K 线。
  • 计算两个唐奇安通道:
    • Entry Period/Entry Shift 用于识别突破水平。
    • Exit Period/Exit Shift 用于跟踪持仓并移动止损。
  • 当账户为空仓或持有空头时,在移位后的上轨上方一格挂买入止损单。
  • 当账户为空仓或持有多头时,在移位后的下轨下方一格挂卖出止损单。
  • 使用平均真实波幅 (ATR Period) 平滑 trailing 止损的移动频率。
  • 如果蜡烛收盘价低于跟踪的下轨则平多仓;若收盘价高于跟踪的上轨则平空仓。

风险管理

  • Risk Per Trade % 按当前资产净值、最小价格变动和每档价格计算下单数量,并按照交易所的最小手数与增量进行取整,限制在最小/最大允许数量内。
  • 防护性止损使用跟踪通道并叠加 ATR 缓冲,避免频繁修改订单。

Prop Firm 挑战规则

  • 勾选 Use Challenge Rules 后启用挑战检查。
  • 当权益达到 Pass Criteria 时停止交易,取消所有挂单并平掉持仓。
  • 当当日亏损超过 Daily Loss Limit 时立即清空持仓,取消挂单,并在该交易日剩余时间内禁止新订单。每日开始时重新记录基准权益。

参数说明

名称 描述
Entry Period 突破唐奇安通道的回溯周期。
Entry Shift 计算突破时忽略的已完成 K 线数量。
Exit Period 跟踪唐奇安通道的回溯周期。
Exit Shift 计算 trailing 止损时忽略的已完成 K 线数量。
Risk Per Trade % 每次入场风险占账户权益的百分比。
ATR Period 平滑 trailing 止损的 ATR 周期。
Use Challenge Rules 是否启用 prop firm 挑战限制。
Pass Criteria 达到该权益后停止交易。
Daily Loss Limit 当日允许的最大亏损额。
Candle Type 用于计算的 K 线类型。

注意事项

  • 策略需要可用的投资组合数据来计算头寸规模和挑战指标。
  • 每根完成的 K 线都会重新计算挂单价格并取消旧订单。
  • 默认参数复现了原始 MetaTrader 策略的行为。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that recreates the MetaTrader "Prop Firm Helper" expert advisor.
/// Uses Donchian channel breakouts for entry signals with market orders.
/// </summary>
public class PropFirmHelperStrategy : Strategy
{
	private readonly StrategyParam<int> _entryPeriod;
	private readonly StrategyParam<int> _exitPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _signalCooldownBars;

	private DonchianChannels _entryChannel;
	private DonchianChannels _exitChannel;

	private decimal _entryUpper;
	private decimal _entryLower;
	private decimal _exitLower;
	private decimal _exitUpper;
	private decimal _prevEntryUpper;
	private decimal _prevEntryLower;
	private bool _hasValues;
	private decimal _entryPrice;
	private int _cooldownRemaining;

	/// <summary>
	/// Initializes a new instance of <see cref="PropFirmHelperStrategy"/>.
	/// </summary>
	public PropFirmHelperStrategy()
	{
		_entryPeriod = Param(nameof(EntryPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Entry Period", "Number of candles used for breakout Donchian channel", "Entries");

		_exitPeriod = Param(nameof(ExitPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Exit Period", "Number of candles used for trailing Donchian channel", "Exits");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for Donchian calculations", "General");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
			.SetNotNegative()
			.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new breakout entry", "General");
	}

	/// <summary>
	/// Donchian breakout lookback length.
	/// </summary>
	public int EntryPeriod
	{
		get => _entryPeriod.Value;
		set => _entryPeriod.Value = value;
	}

	/// <summary>
	/// Donchian trailing lookback length.
	/// </summary>
	public int ExitPeriod
	{
		get => _exitPeriod.Value;
		set => _exitPeriod.Value = value;
	}

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

	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

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

		_entryChannel = null;
		_exitChannel = null;
		_entryUpper = 0m;
		_entryLower = 0m;
		_exitLower = 0m;
		_exitUpper = 0m;
		_prevEntryUpper = 0m;
		_prevEntryLower = 0m;
		_hasValues = false;
		_entryPrice = 0m;
		_cooldownRemaining = 0;
	}

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

		_hasValues = false;
		_cooldownRemaining = 0;

		_entryChannel = new DonchianChannels { Length = EntryPeriod };
		_exitChannel = new DonchianChannels { Length = ExitPeriod };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(ProcessCandle)
			.Start();

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

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var entryValue = _entryChannel.Process(new CandleIndicatorValue(_entryChannel, candle));
		var exitValue = _exitChannel.Process(new CandleIndicatorValue(_exitChannel, candle));

		if (!_entryChannel.IsFormed || !_exitChannel.IsFormed)
			return;

		if (entryValue is not DonchianChannelsValue entryBands || exitValue is not DonchianChannelsValue exitBands)
			return;

		if (entryBands.UpperBand is not decimal entryUpper || entryBands.LowerBand is not decimal entryLower)
			return;

		if (exitBands.UpperBand is not decimal exitUpper || exitBands.LowerBand is not decimal exitLower)
			return;

		_prevEntryUpper = _entryUpper;
		_prevEntryLower = _entryLower;
		_entryUpper = entryUpper;
		_entryLower = entryLower;
		_exitUpper = exitUpper;
		_exitLower = exitLower;

		if (!_hasValues)
		{
			_hasValues = true;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ProcessSignal(candle);
	}

	private void ProcessSignal(ICandleMessage candle)
	{
		var close = candle.ClosePrice;

		// Exit logic
		if (Position > 0 && close < _exitLower)
		{
			SellMarket(Position);
			_cooldownRemaining = SignalCooldownBars;
			return;
		}
		else if (Position < 0 && close > _exitUpper)
		{
			BuyMarket(-Position);
			_cooldownRemaining = SignalCooldownBars;
			return;
		}

		// Entry logic - breakout above previous entry channel upper
		if (_cooldownRemaining == 0 && _prevEntryUpper > 0 && close > _prevEntryUpper && Position <= 0)
		{
			BuyMarket(Volume + (Position < 0 ? -Position : 0m));
			_entryPrice = close;
			_cooldownRemaining = SignalCooldownBars;
		}
		else if (_cooldownRemaining == 0 && _prevEntryLower > 0 && close < _prevEntryLower && Position >= 0)
		{
			SellMarket(Volume + (Position > 0 ? Position : 0m));
			_entryPrice = close;
			_cooldownRemaining = SignalCooldownBars;
		}
	}
}