在 GitHub 上查看

Small Inside Bar 策略

概述

Small Inside Bar Strategy 通过识别压缩后的内部柱形态,跟随随后的方向转换进行交易。原始的 MetaTrader 5 专家顾问已迁移到 StockSharp 高级 API,实现只在完成的 K 线上运作,非常适合喜欢等待波动收缩后突破信号的交易者。

形态定义

策略会检查最近两根已完成 K 线:

  1. 内部柱条件 – 最新收盘的 K 线必须完全位于上一根 K 线的最高价与最低价之间。
  2. 区间比率过滤 – “母柱”(倒数第二根 K 线)的区间必须至少是内部柱区间的指定倍数,默认值为 2:1。
  3. 方向过滤
    • 多头信号:内部柱收阳线,且位于母柱下半部分,同时母柱收阴线。
    • 空头信号:内部柱收阴线,且位于母柱上半部分,同时母柱收阳线。
  4. 可选的反向交易会交换多空解释,几何条件保持不变。

仓位管理

OpenMode 参数再现原始 EA 的开仓模式:

  • AnySignal – 每个信号都发送新的市价单。在存在反向仓位时,由于 StockSharp 采用净额制,订单会部分对冲原有仓位。
  • SwingWithRefill – 进场前先平掉反向持仓,然后允许在同方向上多次加仓。
  • SingleSwing – 市场中最多持有一个方向的仓位;当已有同向仓位时忽略新的信号。

多头和空头可独立启用,反向模式只是将触发方向调换。

参数

名称 默认值 说明
CandleType 1 小时周期 用于识别形态的 K 线类型。
RangeRatioThreshold 2.0 母柱区间与内部柱区间的最小比率。
EnableLong true 是否允许做多。
EnableShort true 是否允许做空。
ReverseSignals false 是否交换多空信号。
OpenMode SwingWithRefill 新信号出现时如何处理已有仓位。

交易流程

  1. 订阅所选 K 线并等待其收盘。
  2. 维护最近两根完成的 K 线以评估形态。
  3. 当满足区间比率与方向过滤后生成交易信号,可选择应用反向模式。
  4. 通过 IsFormedAndOnlineAndAllowTrading 确认交易环境,并检查相应方向是否被允许。
  5. 根据 OpenMode 计算下单数量,并按照策略基础手数发送市价单。
  6. 更新内部历史记录,使最新 K 线参与下一轮判断。

实现细节

  • 调用 StartProtection() 启用内置风险控制(无预设止损或止盈,可按需扩展)。
  • 仅保存最近两根 K 线,不创建额外数据集合,符合高阶 API 的最佳实践。
  • 所有计算都基于收盘数据,避免在未完成的 K 线上产生信号。
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>
/// Implements the "Small Inside Bar" pattern strategy converted from MetaTrader 5.
/// The strategy searches for an inside bar with a small range compared to the mother bar
/// and opens positions following the direction of the pattern conditions.
/// </summary>
public class SmallInsideBarStrategy : Strategy
{
	/// <summary>
	/// Defines how the strategy manages simultaneous entries.
	/// </summary>
	public enum SmallInsideBarOpenModes
	{
		/// <summary>
		/// Open a new position on every signal without forcing opposite positions to close.
		/// </summary>
		AnySignal,

		/// <summary>
		/// Close opposite positions first and allow adding to the current swing direction.
		/// </summary>
		SwingWithRefill,

		/// <summary>
		/// Maintain a single position in the market and ignore additional entries while it is active.
		/// </summary>
		SingleSwing
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _rangeRatioThreshold;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<SmallInsideBarOpenModes> _openMode;

	private ICandleMessage _previousCandle;
	private ICandleMessage _twoBackCandle;

	/// <summary>
	/// Type of candles used for pattern detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Minimum ratio between the mother bar range and the inside bar range.
	/// </summary>
	public decimal RangeRatioThreshold
	{
		get => _rangeRatioThreshold.Value;
		set => _rangeRatioThreshold.Value = value;
	}

	/// <summary>
	/// Allow long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Allow short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Reverse long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Mode for handling position entries.
	/// </summary>
	public SmallInsideBarOpenModes OpenMode
	{
		get => _openMode.Value;
		set => _openMode.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public SmallInsideBarStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for pattern detection", "General");

		_rangeRatioThreshold = Param(nameof(RangeRatioThreshold), 2.25m)
			.SetGreaterThanZero()
			.SetDisplay("Range Ratio", "Minimum mother-to-inside bar range ratio", "Pattern")
			
			.SetOptimize(1.5m, 3m, 0.25m);

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow bullish trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow bearish trades", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short signals", "Trading");

		_openMode = Param(nameof(OpenMode), SmallInsideBarOpenModes.SwingWithRefill)
			.SetDisplay("Open Mode", "Position management mode", "Trading");
	}

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

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

		_previousCandle = null;
		_twoBackCandle = null;
	}

	/// <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 void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		if (_twoBackCandle == null)
		{
			_twoBackCandle = _previousCandle;
			_previousCandle = candle;
			return;
		}

		var insideHigh = _previousCandle.HighPrice;
		var insideLow = _previousCandle.LowPrice;
		var motherHigh = _twoBackCandle.HighPrice;
		var motherLow = _twoBackCandle.LowPrice;

		if (insideHigh <= insideLow || motherHigh <= motherLow)
		{
			ShiftHistory(candle);
			return;
		}

		if (!(insideHigh < motherHigh && insideLow > motherLow))
		{
			ShiftHistory(candle);
			return;
		}

		var insideRange = insideHigh - insideLow;
		var motherRange = motherHigh - motherLow;
		var ratio = insideRange == 0 ? decimal.MaxValue : motherRange / insideRange;

		if (ratio <= RangeRatioThreshold)
		{
			ShiftHistory(candle);
			return;
		}

		var midpoint = (motherHigh + motherLow) / 2m;

		var bullishInside = _previousCandle.ClosePrice > _previousCandle.OpenPrice && insideHigh < midpoint && _twoBackCandle.ClosePrice < _twoBackCandle.OpenPrice;
		var bearishInside = _previousCandle.ClosePrice < _previousCandle.OpenPrice && insideLow < midpoint && _twoBackCandle.ClosePrice > _twoBackCandle.OpenPrice;

		if (ReverseSignals)
		{
			(bullishInside, bearishInside) = (bearishInside, bullishInside);
		}

		var shouldOpenLong = bullishInside && EnableLong;
		var shouldOpenShort = bearishInside && EnableShort;

		if (shouldOpenLong)
		{
			var volume = CalculateOrderVolume(true);

			if (volume > 0)
				BuyMarket(volume);
		}

		if (shouldOpenShort)
		{
			var volume = CalculateOrderVolume(false);

			if (volume > 0)
				SellMarket(volume);
		}

		ShiftHistory(candle);
	}

	private decimal CalculateOrderVolume(bool isLong)
	{
		var baseVolume = Volume;

		if (baseVolume <= 0)
			return 0;

		var position = Position;

		if (isLong)
		{
			if (OpenMode == SmallInsideBarOpenModes.SingleSwing && position > 0)
				return 0;

			if (position < 0 && OpenMode != SmallInsideBarOpenModes.AnySignal)
				baseVolume += Math.Abs(position);
		}
		else
		{
			if (OpenMode == SmallInsideBarOpenModes.SingleSwing && position < 0)
				return 0;

			if (position > 0 && OpenMode != SmallInsideBarOpenModes.AnySignal)
				baseVolume += Math.Abs(position);
		}

		return baseVolume;
	}

	private void ShiftHistory(ICandleMessage candle)
	{
		// Keep track of the last two finished candles for pattern evaluation.
		_twoBackCandle = _previousCandle;
		_previousCandle = candle;
	}
}