Bitcoin Intraday Seasonality
Стратегия, которая покупает биткоин в заранее определённые часы в течение суток.
Тестирование показывает среднегодовую доходность около 45%. Лучше всего работает на крипторынке.
Система анализирует часовые свечи. В выбранные по UTC часы она удерживает длинную позицию, размер которой равен стоимости портфеля. В остальное время позиция закрывается. Приказы меньше минимального USD значения игнорируются.
Детали
- Условия входа: длинная позиция в указанные часы UTC.
- Длинные/короткие: только длинные.
- Условия выхода: выход вне указанных часов.
- Стопы: нет.
- Значения по умолчанию:
HoursLong= [0, 1, 2, 3]MinTradeUsd= 200CandleType= TimeSpan.FromHours(1)
- Фильтры:
- Категория: Сезонность
- Направление: Long
- Индикаторы: Нет
- Стопы: Нет
- Сложность: Базовая
- Таймфрейм: Внутридневной (1ч)
- Сезонность: Да
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// Strategy that goes long on Bitcoin during predefined strong intraday hours.
/// </summary>
public class BitcoinIntradaySeasonalityStrategy : Strategy
{
private readonly StrategyParam<int[]> _hoursLong;
private readonly StrategyParam<decimal> _minUsd;
private readonly StrategyParam<DataType> _candleType;
/// <summary>
/// UTC hours when the strategy holds a long position.
/// </summary>
public int[] HoursLong
{
get => _hoursLong.Value;
set => _hoursLong.Value = value;
}
/// <summary>
/// Minimum trade value in USD.
/// </summary>
public decimal MinTradeUsd
{
get => _minUsd.Value;
set => _minUsd.Value = value;
}
/// <summary>
/// The type of candles to use for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private readonly Dictionary<Security, decimal> _latestPrices = [];
/// <summary>
/// Initializes a new instance of <see cref="BitcoinIntradaySeasonalityStrategy"/>.
/// </summary>
public BitcoinIntradaySeasonalityStrategy()
{
// Hours to stay long (UTC).
_hoursLong = Param(nameof(HoursLong), new[] { 0, 1, 2, 3 })
.SetDisplay("Long Hours", "UTC hours when the strategy stays long", "General");
// Minimum trade size.
_minUsd = Param(nameof(MinTradeUsd), 200m)
.SetGreaterThanZero()
.SetDisplay("Min Trade USD", "Minimum order value in USD", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security == null)
throw new InvalidOperationException("BTC security not set.");
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_latestPrices.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (HoursLong == null || HoursLong.Length == 0)
throw new InvalidOperationException("HoursLong cannot be empty.");
if (Security == null)
throw new InvalidOperationException("BTC security not set.");
SubscribeCandles(CandleType, true, Security)
.Bind(c => ProcessCandle(c, Security))
.Start();
}
private void ProcessCandle(ICandleMessage candle, Security security)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Store the latest closing price for this security
_latestPrices[security] = candle.ClosePrice;
OnHourClose(candle);
}
private void OnHourClose(ICandleMessage c)
{
var hour = c.OpenTime.Hour; // assume server UTC
var inSeason = c.OpenTime.DayOfWeek == DayOfWeek.Monday && c.OpenTime.Day <= 7 && HoursLong.Contains(hour);
var portfolioValue = Portfolio.CurrentValue ?? 0m;
var price = GetLatestPrice(Security);
var tgt = inSeason && price > 0 ? portfolioValue / price : 0m;
var diff = tgt - PositionBy(Security);
if (price <= 0 || Math.Abs(diff) * price < MinTradeUsd)
return;
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Side = diff > 0 ? Sides.Buy : Sides.Sell,
Volume = Math.Abs(diff),
Type = OrderTypes.Market,
Comment = "BTCSeason",
});
}
private decimal GetLatestPrice(Security security)
{
return _latestPrices.TryGetValue(security, out var price) ? price : 0m;
}
private decimal PositionBy(Security s) => GetPositionValue(s, Portfolio) ?? 0;
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, DayOfWeek
from StockSharp.Messages import DataType, CandleStates, Sides, OrderTypes, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Order
from datatype_extensions import *
class bitcoin_intraday_seasonality_strategy(Strategy):
"""
Strategy that goes long on Bitcoin during predefined strong intraday hours.
Only trades on the first Monday of the month during specified UTC hours.
"""
def __init__(self):
super(bitcoin_intraday_seasonality_strategy, self).__init__()
self._hours_long = self.Param("HoursLong", [0, 1, 2, 3]) \
.SetDisplay("Long Hours", "UTC hours when the strategy stays long", "General")
self._min_trade_usd = self.Param("MinTradeUsd", 200.0) \
.SetGreaterThanZero() \
.SetDisplay("Min Trade USD", "Minimum order value in USD", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._latest_price = 0.0
@property
def HoursLong(self):
return self._hours_long.Value
@HoursLong.setter
def HoursLong(self, value):
self._hours_long.Value = value
@property
def MinTradeUsd(self):
return self._min_trade_usd.Value
@MinTradeUsd.setter
def MinTradeUsd(self, value):
self._min_trade_usd.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(bitcoin_intraday_seasonality_strategy, self).OnReseted()
self._latest_price = 0.0
def OnStarted2(self, time):
super(bitcoin_intraday_seasonality_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_price = float(candle.ClosePrice)
hour = candle.OpenTime.Hour
is_first_monday = candle.OpenTime.DayOfWeek == DayOfWeek.Monday and candle.OpenTime.Day <= 7
in_season = is_first_monday and hour in self.HoursLong
price = self._latest_price
if price <= 0:
return
portfolio_value = 0.0
if self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
portfolio_value = float(self.Portfolio.CurrentValue)
if portfolio_value <= 0:
portfolio_value = 100000.0
tgt = portfolio_value / price if in_season else 0.0
diff = tgt - float(self.Position)
if abs(diff) * price < self.MinTradeUsd:
return
if diff > 0:
self.BuyMarket(abs(diff))
elif diff < 0:
self.SellMarket(abs(diff))
def CreateClone(self):
return bitcoin_intraday_seasonality_strategy()