在 GitHub 上查看

Absorption 策略

该策略重现 MetaTrader 平台上的 Absorption 专家顾问。算法寻找吞没前一根 K 线并在近期区间内创出极值的 "吸收" K 线。一旦发现符合条件的 K 线,策略会在其上下放置两条止损挂单,并通过固定目标、保本移动和移动止损来管理仓位。

交易逻辑

  1. 识别形态
    • 检查最近两根已完成的 K 线。
    • 当当前 K 线的最高点高于上一根 K 线的最高点且最低点低于上一根 K 线的最低点时,将其视为 吸收 K 线
    • 进一步确认该 K 线的最高点或最低点是过去 MaxSearch 根 K 线中的极值。
    • 优先使用较老的 K 线(倒数第二根)。如果两根 K 线都满足条件,则使用较老的那根,否则允许使用最近的一根触发信号。
  2. 挂单布置
    • 在吸收 K 线的最高点上方 Indent 个价格步长处放置买入止损单。
    • 在吸收 K 线的最低点下方 Indent 个价格步长处放置卖出止损单。
    • 两个挂单都使用统一的策略手数。
    • 每个挂单保存自己的止损位置以及可选的止盈目标。如果在 OrderExpirationHours 小时内未触发,挂单会被自动取消。
  3. 仓位管理
    • 当一侧挂单成交时,另一侧挂单被取消。
    • 初始止损位于吸收 K 线的另一侧边界并加上/减去设定的 Indent
    • 可选的固定止盈在价格达到指定的步长距离后平仓。
    • 保本模块在价格向有利方向移动 BreakevenProfit 个步长后,将止损移动到 Entry + Breakeven(多头)或 Entry - Breakeven(空头)。
    • 移动止损保持止损与最佳价格之间 TrailingStop 个步长的距离,仅在价格至少向盈利方向移动 TrailingStep 个步长时才更新。

参数

参数 说明
CandleType 使用的 K 线类型(默认 1 小时)。
MaxSearch 用于确认极值的最近 K 线数量。
TakeProfitBuy 多头止盈距离(价格步长)。0 表示关闭止盈。
TakeProfitSell 空头止盈距离(价格步长)。0 表示关闭止盈。
TrailingStop 移动止损距离(价格步长)。0 表示关闭移动止损。
TrailingStep 更新移动止损所需的最小盈利位移,启用移动止损时必须为正。
Indent 在吸收 K 线上下放置挂单的价格步长偏移。
OrderExpirationHours 挂单有效期(小时)。超过该时间未成交的挂单会被取消。
Breakeven 触发保本后止损相对入场价的偏移量。0 表示关闭保本。
BreakevenProfit 启动保本所需的盈利距离(价格步长)。

所有距离类参数均以品种的价格步长为单位。默认下单手数设为 0.1

风险控制

策略使用市价单离场。止损、止盈、保本与移动止损逻辑会监控每根 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>
/// Absorption outside bar breakout strategy.
/// Detects engulfing candles near recent extremes and places stop orders around the pattern.
/// </summary>
public class AbsorptionStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxSearch;
private readonly StrategyParam<decimal> _takeProfitBuy;
private readonly StrategyParam<decimal> _takeProfitSell;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _trailingStep;
private readonly StrategyParam<decimal> _indent;
private readonly StrategyParam<int> _orderExpirationHours;
private readonly StrategyParam<decimal> _breakeven;
private readonly StrategyParam<decimal> _breakevenProfit;

private Highest _highest;
private Lowest _lowest;

private ICandleMessage _prev1;
private ICandleMessage _prev2;

private bool _hasActiveOrders;
private decimal _pendingHigh;
private decimal _pendingLow;
private decimal _pendingBuyPrice;
private decimal _pendingSellPrice;
private decimal _pendingBuyStopLoss;
private decimal _pendingSellStopLoss;
private decimal _pendingBuyTakeProfit;
private decimal _pendingSellTakeProfit;
private DateTimeOffset? _ordersExpiry;

private decimal _entryPrice;
private decimal _stopLoss;
private decimal _takeProfit;
private decimal _prevPosition;
private bool _exitRequestActive;

/// <summary>
/// Candle type to analyze.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}

/// <summary>
/// Number of candles to inspect for extreme prices.
/// </summary>
public int MaxSearch
{
get => _maxSearch.Value;
set => _maxSearch.Value = value;
}

/// <summary>
/// Take profit distance for long trades in price steps.
/// </summary>
public decimal TakeProfitBuy
{
get => _takeProfitBuy.Value;
set => _takeProfitBuy.Value = value;
}

/// <summary>
/// Take profit distance for short trades in price steps.
/// </summary>
public decimal TakeProfitSell
{
get => _takeProfitSell.Value;
set => _takeProfitSell.Value = value;
}

/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public decimal TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}

/// <summary>
/// Minimal step for trailing stop updates in price steps.
/// </summary>
public decimal TrailingStep
{
get => _trailingStep.Value;
set => _trailingStep.Value = value;
}

/// <summary>
/// Indent distance around the reference candle in price steps.
/// </summary>
public decimal Indent
{
get => _indent.Value;
set => _indent.Value = value;
}

/// <summary>
/// Pending order expiration in hours.
/// </summary>
public int OrderExpirationHours
{
get => _orderExpirationHours.Value;
set => _orderExpirationHours.Value = value;
}

/// <summary>
/// Distance to move stop-loss to breakeven in price steps.
/// </summary>
public decimal Breakeven
{
get => _breakeven.Value;
set => _breakeven.Value = value;
}

/// <summary>
/// Profit needed before breakeven activation in price steps.
/// </summary>
public decimal BreakevenProfit
{
get => _breakevenProfit.Value;
set => _breakevenProfit.Value = value;
}

/// <summary>
/// Initializes <see cref="AbsorptionStrategy"/>.
/// </summary>
public AbsorptionStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to process", "General");

_maxSearch = Param(nameof(MaxSearch), 10)
.SetGreaterThanZero()
.SetDisplay("Search Depth", "Bars to inspect for extremes", "Pattern");

_takeProfitBuy = Param(nameof(TakeProfitBuy), 10m)
.SetNotNegative()
.SetDisplay("Long TP", "Take profit for long trades (steps)", "Risk");

_takeProfitSell = Param(nameof(TakeProfitSell), 10m)
.SetNotNegative()
.SetDisplay("Short TP", "Take profit for short trades (steps)", "Risk");

_trailingStop = Param(nameof(TrailingStop), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance (steps)", "Risk");

_trailingStep = Param(nameof(TrailingStep), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step", "Minimal move to update trailing stop (steps)", "Risk");

_indent = Param(nameof(Indent), 1m)
.SetNotNegative()
.SetDisplay("Indent", "Offset from high/low for entries (steps)", "Pattern");

_orderExpirationHours = Param(nameof(OrderExpirationHours), 8)
.SetGreaterThanZero()
.SetDisplay("Order Expiration", "Validity of pending orders in hours", "Pattern");

_breakeven = Param(nameof(Breakeven), 1m)
.SetNotNegative()
.SetDisplay("Breakeven", "Stop offset once breakeven triggers (steps)", "Risk");

_breakevenProfit = Param(nameof(BreakevenProfit), 10m)
.SetNotNegative()
.SetDisplay("Breakeven Profit", "Profit needed before moving to breakeven (steps)", "Risk");

Volume = 0.1m;
}

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

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

_prev1 = null;
_prev2 = null;
_hasActiveOrders = false;
_ordersExpiry = null;
_pendingHigh = 0m;
_pendingLow = 0m;
_pendingBuyPrice = 0m;
_pendingSellPrice = 0m;
_pendingBuyStopLoss = 0m;
_pendingSellStopLoss = 0m;
_pendingBuyTakeProfit = 0m;
_pendingSellTakeProfit = 0m;
_entryPrice = 0m;
_stopLoss = 0m;
_takeProfit = 0m;
_prevPosition = 0m;
_exitRequestActive = false;
}

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

if (TrailingStop > 0m && TrailingStep <= 0m)
throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");

if (Breakeven > 0m)
{
if (BreakevenProfit <= 0m)
throw new InvalidOperationException("Breakeven profit must be positive when breakeven is enabled.");

if (BreakevenProfit <= Breakeven)
throw new InvalidOperationException("Breakeven profit must exceed breakeven distance.");
}

_highest = new Highest { Length = MaxSearch };
_lowest = new Lowest { Length = MaxSearch };

var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandleRaw)
.Start();
}

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

var highResult = _highest.Process(candle);
var lowResult = _lowest.Process(candle);

if (highResult.IsEmpty || lowResult.IsEmpty || !_highest.IsFormed || !_lowest.IsFormed)
{
UpdatePreviousCandles(candle);
_prevPosition = Position;
return;
}

var highestValue = highResult.ToDecimal();
var lowestValue = lowResult.ToDecimal();

ManageActivePosition(candle);

// Check if pending breakout orders should be triggered
if (_hasActiveOrders)
{
if (_ordersExpiry.HasValue && candle.CloseTime >= _ordersExpiry.Value)
{
ClearPendingOrders();
}
else
{
TryTriggerPendingOrders(candle);
}
}

if (Position == 0 && !_hasActiveOrders && _prev1 != null && _prev2 != null)
{
TryPlaceOrders(candle, highestValue, lowestValue);
}

UpdatePreviousCandles(candle);

if (Position != 0 && _hasActiveOrders)
{
ClearPendingOrders();
}

_prevPosition = Position;
}

private void TryTriggerPendingOrders(ICandleMessage candle)
{
if (Position != 0)
return;

// Check if price has broken above the pending buy level
if (_pendingBuyPrice > 0 && candle.HighPrice >= _pendingBuyPrice)
{
BuyMarket(Volume);
_entryPrice = _pendingBuyPrice;
_stopLoss = _pendingBuyStopLoss;
_takeProfit = _pendingBuyTakeProfit;
_exitRequestActive = false;
ClearPendingOrders();
return;
}

// Check if price has broken below the pending sell level
if (_pendingSellPrice > 0 && candle.LowPrice <= _pendingSellPrice)
{
SellMarket(Volume);
_entryPrice = _pendingSellPrice;
_stopLoss = _pendingSellStopLoss;
_takeProfit = _pendingSellTakeProfit;
_exitRequestActive = false;
ClearPendingOrders();
}
}

private void TryPlaceOrders(ICandleMessage candle, decimal highestValue, decimal lowestValue)
{
var prev2Outside = _prev2.HighPrice > _prev1.HighPrice && _prev2.LowPrice < _prev1.LowPrice;
var prev1Outside = _prev1.HighPrice > _prev2.HighPrice && _prev1.LowPrice < _prev2.LowPrice;

var prev2IsExtreme = IsLowestBar(_prev2, _prev1, candle, lowestValue) || IsHighestBar(_prev2, _prev1, candle, highestValue);
var prev1IsExtreme = IsLowestBar(_prev1, _prev2, candle, lowestValue) || IsHighestBar(_prev1, _prev2, candle, highestValue);

if (prev2Outside && prev2IsExtreme)
{
PlaceEntryOrders(_prev2, candle);
}
else if (prev1Outside && prev1IsExtreme)
{
PlaceEntryOrders(_prev1, candle);
}
}

private void PlaceEntryOrders(ICandleMessage patternCandle, ICandleMessage currentCandle)
{
var volume = Volume;

if (volume <= 0m)
return;

var indent = GetPriceOffset(Indent);
var step = Security?.PriceStep ?? 0.0001m;

var buyPrice = patternCandle.HighPrice + indent;
var sellPrice = patternCandle.LowPrice - indent;

if (sellPrice <= 0m)
sellPrice = step;

var buyStopLoss = Math.Max(patternCandle.LowPrice - indent, step);
var sellStopLoss = patternCandle.HighPrice + indent;

var buyTakeOffset = GetPriceOffset(TakeProfitBuy);
var sellTakeOffset = GetPriceOffset(TakeProfitSell);

var buyTakeProfit = buyTakeOffset > 0m ? buyPrice + buyTakeOffset : 0m;
var sellTakeProfit = sellTakeOffset > 0m ? sellPrice - sellTakeOffset : 0m;

// Store pending breakout levels (will be triggered on next candle)

_hasActiveOrders = true;
_pendingHigh = patternCandle.HighPrice;
_pendingLow = patternCandle.LowPrice;
_pendingBuyPrice = buyPrice;
_pendingSellPrice = sellPrice;
_pendingBuyStopLoss = buyStopLoss;
_pendingSellStopLoss = sellStopLoss;
_pendingBuyTakeProfit = buyTakeProfit;
_pendingSellTakeProfit = sellTakeProfit;
_exitRequestActive = false;

_ordersExpiry = OrderExpirationHours > 0
? currentCandle.CloseTime + TimeSpan.FromHours(OrderExpirationHours)
: null;
}


private void ManageActivePosition(ICandleMessage candle)
{
if (_exitRequestActive)
return;

if (Position > 0)
{
UpdateBreakevenLong(candle);
UpdateTrailingLong(candle);

if (_stopLoss > 0m && candle.LowPrice <= _stopLoss)
{
SellMarket(Math.Abs(Position));
_exitRequestActive = true;
return;
}

if (_takeProfit > 0m && candle.HighPrice >= _takeProfit)
{
SellMarket(Math.Abs(Position));
_exitRequestActive = true;
}
}
else if (Position < 0)
{
UpdateBreakevenShort(candle);
UpdateTrailingShort(candle);

if (_stopLoss > 0m && candle.HighPrice >= _stopLoss)
{
BuyMarket(Math.Abs(Position));
_exitRequestActive = true;
return;
}

if (_takeProfit > 0m && candle.LowPrice <= _takeProfit)
{
BuyMarket(Math.Abs(Position));
_exitRequestActive = true;
}
}
}

private void UpdateBreakevenLong(ICandleMessage candle)
{
if (Breakeven <= 0m || BreakevenProfit <= 0m)
return;

if (_stopLoss >= _entryPrice + GetPriceOffset(Breakeven))
return;

if (candle.HighPrice - _entryPrice >= GetPriceOffset(BreakevenProfit))
_stopLoss = _entryPrice + GetPriceOffset(Breakeven);
}

private void UpdateBreakevenShort(ICandleMessage candle)
{
if (Breakeven <= 0m || BreakevenProfit <= 0m)
return;

if (_stopLoss <= _entryPrice - GetPriceOffset(Breakeven))
return;

if (_entryPrice - candle.LowPrice >= GetPriceOffset(BreakevenProfit))
_stopLoss = _entryPrice - GetPriceOffset(Breakeven);
}

private void UpdateTrailingLong(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;

var trailing = GetPriceOffset(TrailingStop);
var step = GetPriceOffset(TrailingStep);
var current = candle.HighPrice;

if (current - _entryPrice <= trailing + step)
return;

if (_stopLoss < current - (trailing + step))
_stopLoss = Math.Max(_stopLoss, current - trailing);
}

private void UpdateTrailingShort(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;

var trailing = GetPriceOffset(TrailingStop);
var step = GetPriceOffset(TrailingStep);
var current = candle.LowPrice;

if (_entryPrice - current <= trailing + step)
return;

if (_stopLoss == 0m || _stopLoss > current + trailing + step)
_stopLoss = current + trailing;
}

private void UpdatePreviousCandles(ICandleMessage candle)
{
_prev2 = _prev1;
_prev1 = candle;
}

private void ClearPendingOrders()
{
_hasActiveOrders = false;
_ordersExpiry = null;
_pendingHigh = 0m;
_pendingLow = 0m;
_pendingBuyPrice = 0m;
_pendingSellPrice = 0m;
_pendingBuyStopLoss = 0m;
_pendingSellStopLoss = 0m;
_pendingBuyTakeProfit = 0m;
_pendingSellTakeProfit = 0m;
}

private bool IsLowestBar(ICandleMessage candidate, ICandleMessage other, ICandleMessage current, decimal lowestValue)
{
if (!AreClose(candidate.LowPrice, lowestValue))
return false;

return candidate.LowPrice < other.LowPrice && candidate.LowPrice < current.LowPrice;
}

private bool IsHighestBar(ICandleMessage candidate, ICandleMessage other, ICandleMessage current, decimal highestValue)
{
if (!AreClose(candidate.HighPrice, highestValue))
return false;

return candidate.HighPrice > other.HighPrice && candidate.HighPrice > current.HighPrice;
}

private decimal GetPriceOffset(decimal value)
{
var step = Security?.PriceStep ?? 0.0001m;
return value * step;
}

private bool AreClose(decimal first, decimal second)
{
var tolerance = (Security?.PriceStep ?? 0.0001m) / 2m;
return Math.Abs(first - second) <= tolerance;
}
}