Стратегия Fibonacci ATR Fusion
Комбинирует отношение покупочного давления для нескольких периодов Фибоначчи с ATR и использует пересечения порогов для входов и выходов. Предусмотрен опциональный поэтапный тейк-профит на основе ATR.
Детали
- Условия входа:
- Лонг: Средневзвешенное пересекает
LongEntryThresholdснизу вверх. - Шорт: Средневзвешенное пересекает
ShortEntryThresholdсверху вниз.
- Лонг: Средневзвешенное пересекает
- Условия выхода:
- Пересечение противоположного выходного порога или разворот позиции.
- Индикаторы:
- Взвешенные коэффициенты покупочного давления/ATR.
- ATR для тейк-профитов.
- Стопы: Нет.
- Значения по умолчанию:
LongEntryThreshold= 58ShortEntryThreshold= 42LongExitThreshold= 42ShortExitThreshold= 58Tp1Atr= 3Tp2Atr= 8Tp3Atr= 14Tp1Percent= 12Tp2Percent= 12Tp3Percent= 12
- Фильтры:
- Следование тренду
- Один таймфрейм
- Индикаторы: ATR
- Стопы: нет
- Сложность: Средняя
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Fibonacci ATR Fusion strategy.
/// For each Fibonacci period 8, 13, 21, 34 and 55 the buying pressure (close minus the true low) summed over the period is divided
/// by the true range summed over the same period. The ratios are averaged with weights 5, 4, 3, 2, 1 and scaled to 0..100. A cross
/// above LongEntryThreshold goes long and a cross below ShortEntryThreshold goes short, reversing an opposite position; a long
/// closes when the average crosses below LongExitThreshold and a short when it crosses above ShortExitThreshold. Three take-profit
/// layers at Tp1Atr, Tp2Atr and Tp3Atr ATRs from the entry each close their percentage of the entry volume.
/// </summary>
public class FibonacciAtrFusionStrategy : Strategy
{
private static readonly int[] _periods = [8, 13, 21, 34, 55];
private static readonly decimal[] _weights = [5m, 4m, 3m, 2m, 1m];
private const int _atrLength = 14;
private readonly StrategyParam<decimal> _longEntryThreshold;
private readonly StrategyParam<decimal> _shortEntryThreshold;
private readonly StrategyParam<decimal> _longExitThreshold;
private readonly StrategyParam<decimal> _shortExitThreshold;
private readonly StrategyParam<decimal> _tp1Atr;
private readonly StrategyParam<decimal> _tp2Atr;
private readonly StrategyParam<decimal> _tp3Atr;
private readonly StrategyParam<decimal> _tp1Percent;
private readonly StrategyParam<decimal> _tp2Percent;
private readonly StrategyParam<decimal> _tp3Percent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal bp, decimal tr)> _history = [];
private decimal? _prevClose;
private decimal? _prevAverage;
private decimal _entryPrice;
private decimal _entryAtr;
private decimal _entryVolume;
private readonly bool[] _tpDone = new bool[3];
/// <summary>
/// Level the average crosses upward to go long.
/// </summary>
public decimal LongEntryThreshold
{
get => _longEntryThreshold.Value;
set => _longEntryThreshold.Value = value;
}
/// <summary>
/// Level the average crosses downward to go short.
/// </summary>
public decimal ShortEntryThreshold
{
get => _shortEntryThreshold.Value;
set => _shortEntryThreshold.Value = value;
}
/// <summary>
/// Level the average crosses downward to close a long.
/// </summary>
public decimal LongExitThreshold
{
get => _longExitThreshold.Value;
set => _longExitThreshold.Value = value;
}
/// <summary>
/// Level the average crosses upward to close a short.
/// </summary>
public decimal ShortExitThreshold
{
get => _shortExitThreshold.Value;
set => _shortExitThreshold.Value = value;
}
/// <summary>
/// First take profit distance in ATRs.
/// </summary>
public decimal Tp1Atr
{
get => _tp1Atr.Value;
set => _tp1Atr.Value = value;
}
/// <summary>
/// Second take profit distance in ATRs.
/// </summary>
public decimal Tp2Atr
{
get => _tp2Atr.Value;
set => _tp2Atr.Value = value;
}
/// <summary>
/// Third take profit distance in ATRs.
/// </summary>
public decimal Tp3Atr
{
get => _tp3Atr.Value;
set => _tp3Atr.Value = value;
}
/// <summary>
/// Percent of the entry volume closed at the first take profit.
/// </summary>
public decimal Tp1Percent
{
get => _tp1Percent.Value;
set => _tp1Percent.Value = value;
}
/// <summary>
/// Percent of the entry volume closed at the second take profit.
/// </summary>
public decimal Tp2Percent
{
get => _tp2Percent.Value;
set => _tp2Percent.Value = value;
}
/// <summary>
/// Percent of the entry volume closed at the third take profit.
/// </summary>
public decimal Tp3Percent
{
get => _tp3Percent.Value;
set => _tp3Percent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public FibonacciAtrFusionStrategy()
{
_longEntryThreshold = Param(nameof(LongEntryThreshold), 58m)
.SetDisplay("Long Entry", "Level the average crosses upward to go long", "Signals");
_shortEntryThreshold = Param(nameof(ShortEntryThreshold), 42m)
.SetDisplay("Short Entry", "Level the average crosses downward to go short", "Signals");
_longExitThreshold = Param(nameof(LongExitThreshold), 42m)
.SetDisplay("Long Exit", "Level the average crosses downward to close a long", "Signals");
_shortExitThreshold = Param(nameof(ShortExitThreshold), 58m)
.SetDisplay("Short Exit", "Level the average crosses upward to close a short", "Signals");
_tp1Atr = Param(nameof(Tp1Atr), 3m)
.SetNotNegative()
.SetDisplay("TP1 ATR", "First take profit distance in ATRs", "Take Profit");
_tp2Atr = Param(nameof(Tp2Atr), 8m)
.SetNotNegative()
.SetDisplay("TP2 ATR", "Second take profit distance in ATRs", "Take Profit");
_tp3Atr = Param(nameof(Tp3Atr), 14m)
.SetNotNegative()
.SetDisplay("TP3 ATR", "Third take profit distance in ATRs", "Take Profit");
_tp1Percent = Param(nameof(Tp1Percent), 12m)
.SetNotNegative()
.SetDisplay("TP1 %", "Percent of the entry volume closed at the first take profit", "Take Profit");
_tp2Percent = Param(nameof(Tp2Percent), 12m)
.SetNotNegative()
.SetDisplay("TP2 %", "Percent of the entry volume closed at the second take profit", "Take Profit");
_tp3Percent = Param(nameof(Tp3Percent), 12m)
.SetNotNegative()
.SetDisplay("TP3 %", "Percent of the entry volume closed at the third take profit", "Take Profit");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_history.Clear();
_prevClose = null;
_prevAverage = null;
ResetEntry(0m, 0m, 0m);
}
private void ResetEntry(decimal price, decimal atr, decimal volume)
{
_entryPrice = price;
_entryAtr = atr;
_entryVolume = volume;
Array.Clear(_tpDone);
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var atr = new AverageTrueRange { Length = _atrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
var average = UpdateAverage(candle);
if (average is not decimal current)
return;
var prev = _prevAverage;
_prevAverage = current;
if (prev is not decimal last)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var longEntry = last <= LongEntryThreshold && current > LongEntryThreshold;
var shortEntry = last >= ShortEntryThreshold && current < ShortEntryThreshold;
var longExit = last >= LongExitThreshold && current < LongExitThreshold;
var shortExit = last <= ShortExitThreshold && current > ShortExitThreshold;
if (longEntry && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
ResetEntry(candle.ClosePrice, atr, Volume);
return;
}
if (shortEntry && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
ResetEntry(candle.ClosePrice, atr, Volume);
return;
}
if (Position > 0 && longExit)
{
SellMarket(Position);
return;
}
if (Position < 0 && shortExit)
{
BuyMarket(-Position);
return;
}
if (Position != 0)
CheckTakeProfits(candle);
}
private decimal? UpdateAverage(ICandleMessage candle)
{
var prevClose = _prevClose;
_prevClose = candle.ClosePrice;
if (prevClose is not decimal lastClose)
return null;
var trueLow = Math.Min(candle.LowPrice, lastClose);
var trueHigh = Math.Max(candle.HighPrice, lastClose);
_history.Add((candle.ClosePrice - trueLow, trueHigh - trueLow));
var maxPeriod = _periods[^1];
if (_history.Count > maxPeriod)
_history.RemoveAt(0);
if (_history.Count < maxPeriod)
return null;
var weighted = 0m;
var totalWeight = 0m;
for (var i = 0; i < _periods.Length; i++)
{
var period = _periods[i];
var bpSum = 0m;
var trSum = 0m;
for (var j = _history.Count - period; j < _history.Count; j++)
{
bpSum += _history[j].bp;
trSum += _history[j].tr;
}
if (trSum <= 0)
return null;
weighted += _weights[i] * bpSum / trSum;
totalWeight += _weights[i];
}
return weighted / totalWeight * 100m;
}
private void CheckTakeProfits(ICandleMessage candle)
{
if (_entryAtr <= 0)
return;
var distances = new[] { Tp1Atr, Tp2Atr, Tp3Atr };
var percents = new[] { Tp1Percent, Tp2Percent, Tp3Percent };
for (var i = 0; i < distances.Length; i++)
{
if (_tpDone[i] || distances[i] <= 0 || percents[i] <= 0)
continue;
var isLong = Position > 0;
var target = isLong ? _entryPrice + distances[i] * _entryAtr : _entryPrice - distances[i] * _entryAtr;
var hit = isLong ? candle.HighPrice >= target : candle.LowPrice <= target;
if (!hit)
continue;
_tpDone[i] = true;
var volume = Math.Min(Math.Abs(Position), RoundVolume(_entryVolume * percents[i] / 100m));
if (volume <= 0)
continue;
if (isLong)
SellMarket(volume);
else
BuyMarket(volume);
return;
}
}
private decimal RoundVolume(decimal volume)
{
var step = Security?.VolumeStep ?? 0m;
return step > 0 ? Math.Floor(volume / step) * step : volume;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
PERIODS = (8, 13, 21, 34, 55)
WEIGHTS = (5.0, 4.0, 3.0, 2.0, 1.0)
ATR_LENGTH = 14
class fibonacci_atr_fusion_strategy(Strategy):
"""
Fibonacci ATR Fusion strategy.
For each Fibonacci period 8, 13, 21, 34 and 55 the buying pressure (close minus the true low) summed over the period is divided
by the true range summed over the same period. The ratios are averaged with weights 5, 4, 3, 2, 1 and scaled to 0..100. A cross
above LongEntryThreshold goes long and a cross below ShortEntryThreshold goes short, reversing an opposite position; a long
closes when the average crosses below LongExitThreshold and a short when it crosses above ShortExitThreshold. Three take-profit
layers at Tp1Atr, Tp2Atr and Tp3Atr ATRs from the entry each close their percentage of the entry volume.
"""
def __init__(self):
super(fibonacci_atr_fusion_strategy, self).__init__()
self._long_entry_threshold = self.Param("LongEntryThreshold", 58.0).SetDisplay("Long Entry", "Level the average crosses upward to go long", "Signals")
self._short_entry_threshold = self.Param("ShortEntryThreshold", 42.0).SetDisplay("Short Entry", "Level the average crosses downward to go short", "Signals")
self._long_exit_threshold = self.Param("LongExitThreshold", 42.0).SetDisplay("Long Exit", "Level the average crosses downward to close a long", "Signals")
self._short_exit_threshold = self.Param("ShortExitThreshold", 58.0).SetDisplay("Short Exit", "Level the average crosses upward to close a short", "Signals")
self._tp1_atr = self.Param("Tp1Atr", 3.0).SetNotNegative().SetDisplay("TP1 ATR", "First take profit distance in ATRs", "Take Profit")
self._tp2_atr = self.Param("Tp2Atr", 8.0).SetNotNegative().SetDisplay("TP2 ATR", "Second take profit distance in ATRs", "Take Profit")
self._tp3_atr = self.Param("Tp3Atr", 14.0).SetNotNegative().SetDisplay("TP3 ATR", "Third take profit distance in ATRs", "Take Profit")
self._tp1_percent = self.Param("Tp1Percent", 12.0).SetNotNegative().SetDisplay("TP1 %", "Percent of the entry volume closed at the first take profit", "Take Profit")
self._tp2_percent = self.Param("Tp2Percent", 12.0).SetNotNegative().SetDisplay("TP2 %", "Percent of the entry volume closed at the second take profit", "Take Profit")
self._tp3_percent = self.Param("Tp3Percent", 12.0).SetNotNegative().SetDisplay("TP3 %", "Percent of the entry volume closed at the third take profit", "Take Profit")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._history = []
self._prev_close = None
self._prev_average = None
self._reset_entry(0.0, 0.0, 0.0)
def _reset_entry(self, price, atr, volume):
self._entry_price = price
self._entry_atr = atr
self._entry_volume = volume
self._tp_done = [False, False, False]
def OnReseted(self):
super(fibonacci_atr_fusion_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(fibonacci_atr_fusion_strategy, self).OnStarted2(time)
self._reset_state()
atr = AverageTrueRange()
atr.Length = ATR_LENGTH
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
current = self._update_average(candle)
if current is None:
return
last = self._prev_average
self._prev_average = current
if last is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
long_entry_level = float(self._long_entry_threshold.Value)
short_entry_level = float(self._short_entry_threshold.Value)
long_exit_level = float(self._long_exit_threshold.Value)
short_exit_level = float(self._short_exit_threshold.Value)
long_entry = last <= long_entry_level and current > long_entry_level
short_entry = last >= short_entry_level and current < short_entry_level
long_exit = last >= long_exit_level and current < long_exit_level
short_exit = last <= short_exit_level and current > short_exit_level
atr = float(atr_value)
if long_entry and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._reset_entry(float(candle.ClosePrice), atr, float(self.Volume))
return
if short_entry and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._reset_entry(float(candle.ClosePrice), atr, float(self.Volume))
return
if self.Position > 0 and long_exit:
self.SellMarket(self.Position)
return
if self.Position < 0 and short_exit:
self.BuyMarket(-self.Position)
return
if self.Position != 0:
self._check_take_profits(candle)
def _update_average(self, candle):
close = float(candle.ClosePrice)
last_close = self._prev_close
self._prev_close = close
if last_close is None:
return None
true_low = min(float(candle.LowPrice), last_close)
true_high = max(float(candle.HighPrice), last_close)
self._history.append((close - true_low, true_high - true_low))
max_period = PERIODS[-1]
if len(self._history) > max_period:
del self._history[0]
if len(self._history) < max_period:
return None
weighted = 0.0
total_weight = 0.0
for period, weight in zip(PERIODS, WEIGHTS):
window = self._history[-period:]
bp_sum = sum(item[0] for item in window)
tr_sum = sum(item[1] for item in window)
if tr_sum <= 0:
return None
weighted += weight * bp_sum / tr_sum
total_weight += weight
return weighted / total_weight * 100.0
def _check_take_profits(self, candle):
if self._entry_atr <= 0:
return
distances = (float(self._tp1_atr.Value), float(self._tp2_atr.Value), float(self._tp3_atr.Value))
percents = (float(self._tp1_percent.Value), float(self._tp2_percent.Value), float(self._tp3_percent.Value))
for i in range(3):
if self._tp_done[i] or distances[i] <= 0 or percents[i] <= 0:
continue
is_long = self.Position > 0
target = self._entry_price + distances[i] * self._entry_atr if is_long else self._entry_price - distances[i] * self._entry_atr
hit = float(candle.HighPrice) >= target if is_long else float(candle.LowPrice) <= target
if not hit:
continue
self._tp_done[i] = True
volume = min(abs(float(self.Position)), self._round_volume(self._entry_volume * percents[i] / 100.0))
if volume <= 0:
continue
if is_long:
self.SellMarket(Decimal(volume))
else:
self.BuyMarket(Decimal(volume))
return
def _round_volume(self, volume):
step = float(self.Security.VolumeStep) if self.Security is not None and self.Security.VolumeStep is not None else 0.0
return math.floor(volume / step) * step if step > 0 else volume
def CreateClone(self):
return fibonacci_atr_fusion_strategy()