Fib Hurst Breakout
Правила торговли
Исполнение идёт по внутридневному CandleType. Диапазон Фибоначчи — максимум и минимум предыдущих LookbackPeriod завершённых дневных свечей UTC без дня сигнала. Уровни: L+0.618×(H−L) и L+0.382×(H−L). Дневной Hurst по предыдущим HurstPeriod закрытиям: H = log(R/S)/log(N), R — диапазон накопленных отклонений от среднего, S — стандартное отклонение с делителем N. Плоское окно с нулевой дисперсией запрещает вход. Предыдущее и текущее внутридневные закрытия сравниваются с одним текущим дневным уровнем: пересечение вверх 61.8% при H>0.5 покупает, вниз 38.2% при H<0.5 открывает шорт. Вход только из нуля объёмом Volume. MaxTradesPerDay считает попытки входных заявок по исторической дате UTC, MaxTotalTrades — за запуск. Native-защита по котировкам фиксирует от фактического исполнения SL = RiskPercent% и TP = RiskPercent×RiskReward%. Разворота по обратному сигналу и задержки нет.
Параметры
| Параметр | По умолчанию |
|---|---|
CandleType |
TimeSpan.FromMinutes(15).TimeFrame() |
HurstPeriod |
50 |
LookbackPeriod |
20 |
MaxTradesPerDay |
5 |
MaxTotalTrades |
510 |
RiskPercent |
2 |
RiskReward |
2 |
DailyHistoryFile |
не задан |
Входные данные
Необязательный DailyHistoryFile: CSV Date,High,Low,Close, дата yyyy-MM-dd. Даты уникальны, 0 < Low ≤ Close ≤ High. Допускаются только даты раньше дня внутридневного сигнала. Завершённые дневные свечи заменяют свою дату; будущие строки не влияют на прошлые решения.
Проверка
Оба языка проверяются на имеющемся архиве BTC/TON: независимые формулы, стороны и объёмы заявок, реальные исполнения, позиции и выходы. Датированные ценовые/факторные и календарные данные проверяют механику; перенос дат OHLC/объёмов не является многолетней рыночной историей. Две корзины рангов, TopN=1 и SMA=2 — явные профили для малого универсума. Исходные длинные периоды и defaults проверяются отдельно. Доходность не заявляется.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Prior completed daily Fibonacci range and daily R/S Hurst filter with capped intraday entries.</summary>
public class FibHurstBreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _hurstPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<int> _maxTradesPerDay;
private readonly StrategyParam<int> _maxTotalTrades;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _riskReward;
private readonly StrategyParam<string> _dailyHistoryFile;
private Order _pending;
private readonly SortedDictionary<DateTime, (decimal high, decimal low, decimal close)> _daily = [];
private decimal? _previousClose;
private DateTime _day;
private int _dailyTrades;
private int _totalTrades;
/// <summary>Finished signal candle type.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>Completed daily closes in the R/S Hurst window.</summary>
public int HurstPeriod { get => _hurstPeriod.Value; set => _hurstPeriod.Value = value; }
/// <summary>Prior completed daily bars in the Fibonacci range.</summary>
public int LookbackPeriod { get => _lookbackPeriod.Value; set => _lookbackPeriod.Value = value; }
/// <summary>Maximum filled-entry attempts per historical UTC day.</summary>
public int MaxTradesPerDay { get => _maxTradesPerDay.Value; set => _maxTradesPerDay.Value = value; }
/// <summary>Maximum entry attempts in one run.</summary>
public int MaxTotalTrades { get => _maxTotalTrades.Value; set => _maxTotalTrades.Value = value; }
/// <summary>Native stop distance as a percentage of the actual fill.</summary>
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
/// <summary>Take-profit percentage divided by the stop percentage.</summary>
public decimal RiskReward { get => _riskReward.Value; set => _riskReward.Value = value; }
/// <summary>Optional dated daily High/Low/Close CSV for warmup.</summary>
public string DailyHistoryFile { get => _dailyHistoryFile.Value; set => _dailyHistoryFile.Value = value; }
/// <summary>Initialize the strategy parameters.</summary>
public FibHurstBreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("CandleType", "Finished signal candle type.", "Parameters");
_hurstPeriod = Param(nameof(HurstPeriod), 50).SetRange(2, int.MaxValue)
.SetDisplay("HurstPeriod", "Completed daily closes in the R/S Hurst window.", "Parameters");
_lookbackPeriod = Param(nameof(LookbackPeriod), 20).SetRange(1, int.MaxValue)
.SetDisplay("LookbackPeriod", "Prior completed daily bars in the Fibonacci range.", "Parameters");
_maxTradesPerDay = Param(nameof(MaxTradesPerDay), 5).SetRange(1, int.MaxValue)
.SetDisplay("MaxTradesPerDay", "Maximum filled-entry attempts per historical UTC day.", "Parameters");
_maxTotalTrades = Param(nameof(MaxTotalTrades), 510).SetRange(1, int.MaxValue)
.SetDisplay("MaxTotalTrades", "Maximum entry attempts in one run.", "Parameters");
_riskPercent = Param(nameof(RiskPercent), 2m).SetRange(0.000001m, decimal.MaxValue)
.SetDisplay("RiskPercent", "Native stop distance as a percentage of the actual fill.", "Parameters");
_riskReward = Param(nameof(RiskReward), 2m).SetRange(0.000001m, decimal.MaxValue)
.SetDisplay("RiskReward", "Take-profit percentage divided by the stop percentage.", "Parameters");
_dailyHistoryFile = Param<string>(nameof(DailyHistoryFile))
.SetDisplay("DailyHistoryFile", "Optional dated daily High/Low/Close CSV for warmup.", "Parameters");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, TimeSpan.FromDays(1).TimeFrame())];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pending = null;
_daily.Clear();
_previousClose = null;
_day = default;
_dailyTrades = 0;
_totalTrades = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (CandleType.Arg is not TimeSpan frame || frame <= TimeSpan.Zero || frame >= TimeSpan.FromDays(1))
throw new InvalidOperationException("CandleType must be an intraday time frame.");
LoadDailyHistory();
base.OnStarted2(time);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Start();
}
SubscribeCandles(TimeSpan.FromDays(1).TimeFrame()).Bind(candle =>
{
if (candle.State == CandleStates.Finished)
_daily[candle.OpenTime.Date] = (candle.HighPrice, candle.LowPrice, candle.ClosePrice);
}).Start();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private bool IsPending => _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed);
private void Submit(Sides side, decimal quantity, string comment)
{
_pending = CreateOrder(side, 0m, quantity);
_pending.Type = OrderTypes.Market;
_pending.Comment = comment;
RegisterOrder(_pending);
}
private void LoadDailyHistory()
{
if (string.IsNullOrWhiteSpace(DailyHistoryFile)) return;
using var lines = File.ReadLines(DailyHistoryFile).GetEnumerator();
if (!lines.MoveNext() || lines.Current.Trim() != "Date,High,Low,Close")
throw new InvalidOperationException("Daily CSV header must be Date,High,Low,Close.");
while (lines.MoveNext())
{
if (string.IsNullOrWhiteSpace(lines.Current)) continue;
var row = lines.Current.Split(',');
if (row.Length != 4) throw new InvalidOperationException("Invalid daily CSV field count.");
var date = DateTime.ParseExact(row[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture);
var high = decimal.Parse(row[1], CultureInfo.InvariantCulture);
var low = decimal.Parse(row[2], CultureInfo.InvariantCulture);
var close = decimal.Parse(row[3], CultureInfo.InvariantCulture);
if (low <= 0m || high < low || close < low || close > high || !_daily.TryAdd(date, (high, low, close)))
throw new InvalidOperationException("Daily history must contain unique dates and valid positive OHLC.");
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished) return;
var previous = _previousClose;
_previousClose = candle.ClosePrice;
if (_day != candle.OpenTime.Date)
{
_day = candle.OpenTime.Date;
_dailyTrades = 0;
}
var days = _daily.Where(d => d.Key < _day).Select(d => d.Value).ToArray();
if (days.Length < Math.Max(HurstPeriod, LookbackPeriod) || previous is null || Position != 0m || IsPending
|| _dailyTrades >= MaxTradesPerDay || _totalTrades >= MaxTotalTrades || !IsFormedAndOnlineAndAllowTrading()) return;
var prices = days.TakeLast(HurstPeriod).Select(d => (double)d.close).ToArray();
var mean = prices.Average();
var variance = prices.Sum(p => (p - mean) * (p - mean)) / prices.Length;
if (variance <= 0d) return;
var cumulative = 0d;
var minimum = double.MaxValue;
var maximum = double.MinValue;
foreach (var price in prices)
{
cumulative += price - mean;
minimum = Math.Min(minimum, cumulative);
maximum = Math.Max(maximum, cumulative);
}
var rescaled = (maximum - minimum) / Math.Sqrt(variance);
if (rescaled <= 0d) return;
var hurst = Math.Log(rescaled) / Math.Log(prices.Length);
var range = days.TakeLast(LookbackPeriod).ToArray();
var low = range.Min(d => d.low);
var high = range.Max(d => d.high);
if (high <= low) return;
var upper = low + 0.618m * (high - low);
var lower = low + 0.382m * (high - low);
Sides? side = previous <= upper && candle.ClosePrice > upper && hurst > 0.5d ? Sides.Buy
: previous >= lower && candle.ClosePrice < lower && hurst < 0.5d ? Sides.Sell : null;
if (side is null) return;
_dailyTrades++;
_totalTrades++;
StartProtection(new Unit(RiskPercent * RiskReward, UnitTypes.Percent), new Unit(RiskPercent, UnitTypes.Percent),
useMarketOrders: true, isLocalStop: true);
Submit(side.Value, Volume, "0502Entry");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, Decimal, Math, TimeSpan, InvalidOperationException
from System.Globalization import CultureInfo, DateTimeStyles
from System.IO import File
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage, AverageTrueRange, AlligatorLine, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import Level1Fields, ITickTradeMessage, CandleStates, DataType, OrderStates, OrderTypes, Sides, Unit, UnitTypes
class fib_hurst_breakout_strategy(Strategy):
"""Prior completed daily Fibonacci range and daily R/S Hurst filter with capped intraday entries."""
def __init__(self):
super(fib_hurst_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("CandleType", "Finished signal candle type.", "Parameters")
self._hurst_period = self.Param("HurstPeriod", 50).SetRange(2, 2147483647) \
.SetDisplay("HurstPeriod", "Completed daily closes in the R/S Hurst window.", "Parameters")
self._lookback_period = self.Param("LookbackPeriod", 20).SetRange(1, 2147483647) \
.SetDisplay("LookbackPeriod", "Prior completed daily bars in the Fibonacci range.", "Parameters")
self._max_trades_per_day = self.Param("MaxTradesPerDay", 5).SetRange(1, 2147483647) \
.SetDisplay("MaxTradesPerDay", "Maximum filled-entry attempts per historical UTC day.", "Parameters")
self._max_total_trades = self.Param("MaxTotalTrades", 510).SetRange(1, 2147483647) \
.SetDisplay("MaxTotalTrades", "Maximum entry attempts in one run.", "Parameters")
self._risk_percent = self.Param("RiskPercent", Decimal(2)).SetRange(Decimal(0.000001), Decimal.MaxValue) \
.SetDisplay("RiskPercent", "Native stop distance as a percentage of the actual fill.", "Parameters")
self._risk_reward = self.Param("RiskReward", Decimal(2)).SetRange(Decimal(0.000001), Decimal.MaxValue) \
.SetDisplay("RiskReward", "Take-profit percentage divided by the stop percentage.", "Parameters")
self._daily_history_file = self.Param[str]("DailyHistoryFile", None) \
.SetDisplay("DailyHistoryFile", "Optional dated daily High/Low/Close CSV for warmup.", "Parameters")
self._clear_state()
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def HurstPeriod(self):
return self._hurst_period.Value
@HurstPeriod.setter
def HurstPeriod(self, value):
self._hurst_period.Value = value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@LookbackPeriod.setter
def LookbackPeriod(self, value):
self._lookback_period.Value = value
@property
def MaxTradesPerDay(self):
return self._max_trades_per_day.Value
@MaxTradesPerDay.setter
def MaxTradesPerDay(self, value):
self._max_trades_per_day.Value = value
@property
def MaxTotalTrades(self):
return self._max_total_trades.Value
@MaxTotalTrades.setter
def MaxTotalTrades(self, value):
self._max_total_trades.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def RiskReward(self):
return self._risk_reward.Value
@RiskReward.setter
def RiskReward(self, value):
self._risk_reward.Value = value
@property
def DailyHistoryFile(self):
return self._daily_history_file.Value
@DailyHistoryFile.setter
def DailyHistoryFile(self, value):
self._daily_history_file.Value = value
def _clear_state(self):
self._pending = None
self._daily = {}
self._previous_close = None
self._day = None
self._daily_trades = 0
self._total_trades = 0
def OnReseted(self):
super(fib_hurst_breakout_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.TimeFrame(TimeSpan.FromDays(1)))]
def OnStarted2(self, time):
if self.CandleType.Arg <= TimeSpan.Zero or self.CandleType.Arg >= TimeSpan.FromDays(1):
raise InvalidOperationException("CandleType must be an intraday time frame.")
self._load_daily_history()
super(fib_hurst_breakout_strategy, self).OnStarted2(time)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Start()
self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromDays(1))).Bind(self._process_daily).Start()
self.SubscribeCandles(self.CandleType).Bind(self._process_candle).Start()
def _is_pending(self):
return self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed)
def _submit(self, side, quantity, comment):
self._pending = self.CreateOrder(side, Decimal(0), quantity)
self._pending.Type = OrderTypes.Market
self._pending.Comment = comment
self.RegisterOrder(self._pending)
@staticmethod
def _process(indicator, candle):
value = CandleIndicatorValue(indicator, candle)
value.IsFinal = True
result = indicator.Process(value)
return None if result.IsEmpty else result.GetValue[Decimal](None)
def _load_daily_history(self):
if not self.DailyHistoryFile:
return
lines = list(File.ReadLines(self.DailyHistoryFile))
if not lines or lines[0].strip() != "Date,High,Low,Close":
raise InvalidOperationException("Daily CSV header must be Date,High,Low,Close.")
for line in lines[1:]:
if not line.strip():
continue
row = line.split(",")
if len(row) != 4:
raise InvalidOperationException("Invalid daily CSV field count.")
date = DateTime.ParseExact(row[0].strip(), "yyyy-MM-dd", CultureInfo.InvariantCulture)
high, low, close = [Decimal.Parse(v, CultureInfo.InvariantCulture) for v in row[1:]]
if low <= 0 or high < low or close < low or close > high or date in self._daily:
raise InvalidOperationException("Daily history must contain unique dates and valid positive OHLC.")
self._daily[date] = (high, low, close)
def _process_daily(self, candle):
if candle.State == CandleStates.Finished:
self._daily[candle.OpenTime.Date] = (candle.HighPrice, candle.LowPrice, candle.ClosePrice)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
previous = self._previous_close
self._previous_close = candle.ClosePrice
if self._day != candle.OpenTime.Date:
self._day = candle.OpenTime.Date
self._daily_trades = 0
days = [self._daily[d] for d in sorted(self._daily) if d < self._day]
if len(days) < max(self.HurstPeriod, self.LookbackPeriod) or previous is None or self.Position != 0 or self._is_pending() or self._daily_trades >= self.MaxTradesPerDay or self._total_trades >= self.MaxTotalTrades or not self.IsFormedAndOnlineAndAllowTrading():
return
prices = [float(d[2]) for d in days[-self.HurstPeriod:]]
mean = sum(prices) / len(prices)
variance = sum((p - mean) * (p - mean) for p in prices) / len(prices)
if variance <= 0:
return
cumulative = 0.0
path = []
for price in prices:
cumulative += price - mean
path.append(cumulative)
rescaled = (max(path) - min(path)) / Math.Sqrt(variance)
if rescaled <= 0:
return
hurst = Math.Log(rescaled) / Math.Log(float(len(prices)))
window = days[-self.LookbackPeriod:]
low = min(d[1] for d in window)
high = max(d[0] for d in window)
if high <= low:
return
upper = low + Decimal(0.618) * (high - low)
lower = low + Decimal(0.382) * (high - low)
side = Sides.Buy if previous <= upper and candle.ClosePrice > upper and hurst > 0.5 else Sides.Sell if previous >= lower and candle.ClosePrice < lower and hurst < 0.5 else None
if side is None:
return
self._daily_trades += 1
self._total_trades += 1
self.StartProtection(Unit(self.RiskPercent * self.RiskReward, UnitTypes.Percent), Unit(self.RiskPercent, UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
self._submit(side, self.Volume, "0502Entry")
def CreateClone(self):
return fib_hurst_breakout_strategy()