Small Inside Bar 策略
概述
Small Inside Bar Strategy 通过识别压缩后的内部柱形态,跟随随后的方向转换进行交易。原始的 MetaTrader 5 专家顾问已迁移到 StockSharp 高级 API,实现只在完成的 K 线上运作,非常适合喜欢等待波动收缩后突破信号的交易者。
形态定义
策略会检查最近两根已完成 K 线:
- 内部柱条件 – 最新收盘的 K 线必须完全位于上一根 K 线的最高价与最低价之间。
- 区间比率过滤 – “母柱”(倒数第二根 K 线)的区间必须至少是内部柱区间的指定倍数,默认值为 2:1。
- 方向过滤 –
- 多头信号:内部柱收阳线,且位于母柱下半部分,同时母柱收阴线。
- 空头信号:内部柱收阴线,且位于母柱上半部分,同时母柱收阳线。
- 可选的反向交易会交换多空解释,几何条件保持不变。
仓位管理
OpenMode 参数再现原始 EA 的开仓模式:
- AnySignal – 每个信号都发送新的市价单。在存在反向仓位时,由于 StockSharp 采用净额制,订单会部分对冲原有仓位。
- SwingWithRefill – 进场前先平掉反向持仓,然后允许在同方向上多次加仓。
- SingleSwing – 市场中最多持有一个方向的仓位;当已有同向仓位时忽略新的信号。
多头和空头可独立启用,反向模式只是将触发方向调换。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CandleType |
1 小时周期 | 用于识别形态的 K 线类型。 |
RangeRatioThreshold |
2.0 | 母柱区间与内部柱区间的最小比率。 |
EnableLong |
true | 是否允许做多。 |
EnableShort |
true | 是否允许做空。 |
ReverseSignals |
false | 是否交换多空信号。 |
OpenMode |
SwingWithRefill | 新信号出现时如何处理已有仓位。 |
交易流程
- 订阅所选 K 线并等待其收盘。
- 维护最近两根完成的 K 线以评估形态。
- 当满足区间比率与方向过滤后生成交易信号,可选择应用反向模式。
- 通过
IsFormedAndOnlineAndAllowTrading确认交易环境,并检查相应方向是否被允许。 - 根据
OpenMode计算下单数量,并按照策略基础手数发送市价单。 - 更新内部历史记录,使最新 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class small_inside_bar_strategy(Strategy):
def __init__(self):
super(small_inside_bar_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._range_ratio_threshold = self.Param("RangeRatioThreshold", 2.25)
self._enable_long = self.Param("EnableLong", True)
self._enable_short = self.Param("EnableShort", True)
self._reverse_signals = self.Param("ReverseSignals", False)
self._prev_candle_high = None
self._prev_candle_low = None
self._prev_candle_open = None
self._prev_candle_close = None
self._two_back_high = None
self._two_back_low = None
self._two_back_open = None
self._two_back_close = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(small_inside_bar_strategy, self).OnStarted2(time)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._prev_candle_high is None:
self._cache_prev(candle)
return
if self._two_back_high is None:
self._shift_history(candle)
return
inside_high = self._prev_candle_high
inside_low = self._prev_candle_low
mother_high = self._two_back_high
mother_low = self._two_back_low
if inside_high <= inside_low or mother_high <= mother_low:
self._shift_history(candle)
return
if not (inside_high < mother_high and inside_low > mother_low):
self._shift_history(candle)
return
inside_range = inside_high - inside_low
mother_range = mother_high - mother_low
ratio = mother_range / inside_range if inside_range != 0 else 1e18
if ratio <= self._range_ratio_threshold.Value:
self._shift_history(candle)
return
midpoint = (mother_high + mother_low) / 2.0
bullish_inside = (self._prev_candle_close > self._prev_candle_open and
inside_high < midpoint and
self._two_back_close < self._two_back_open)
bearish_inside = (self._prev_candle_close < self._prev_candle_open and
inside_low < midpoint and
self._two_back_close > self._two_back_open)
if self._reverse_signals.Value:
bullish_inside, bearish_inside = bearish_inside, bullish_inside
should_open_long = bullish_inside and self._enable_long.Value
should_open_short = bearish_inside and self._enable_short.Value
if should_open_long:
volume = self._calc_order_volume(True)
if volume > 0:
self.BuyMarket(volume)
if should_open_short:
volume = self._calc_order_volume(False)
if volume > 0:
self.SellMarket(volume)
self._shift_history(candle)
def _calc_order_volume(self, is_long):
base_volume = float(self.Volume)
if base_volume <= 0:
return 0
position = self.Position
if is_long:
if position < 0:
base_volume += abs(position)
else:
if position > 0:
base_volume += abs(position)
return base_volume
def _cache_prev(self, candle):
self._prev_candle_high = float(candle.HighPrice)
self._prev_candle_low = float(candle.LowPrice)
self._prev_candle_open = float(candle.OpenPrice)
self._prev_candle_close = float(candle.ClosePrice)
def _shift_history(self, candle):
self._two_back_high = self._prev_candle_high
self._two_back_low = self._prev_candle_low
self._two_back_open = self._prev_candle_open
self._two_back_close = self._prev_candle_close
self._cache_prev(candle)
def OnReseted(self):
super(small_inside_bar_strategy, self).OnReseted()
self._prev_candle_high = None
self._prev_candle_low = None
self._prev_candle_open = None
self._prev_candle_close = None
self._two_back_high = None
self._two_back_low = None
self._two_back_open = None
self._two_back_close = None
def CreateClone(self):
return small_inside_bar_strategy()