Ver en GitHub

Estrategia de Estacionalidad Intradía de Bitcoin

Estrategia que toma posiciones largas en Bitcoin durante horas intradía predefinidas de alta actividad.

Las pruebas indican un rendimiento anual promedio de aproximadamente 45%. Funciona mejor en el mercado de criptomonedas.

El sistema monitorea velas horarias. Durante las horas UTC seleccionadas mantiene una posición larga dimensionada al valor del portafolio. Fuera de esas horas sale a efectivo. Se omiten las órdenes inferiores a un valor mínimo en USD.

Detalles

  • Criterios de entrada: Mantener BTC largo durante las horas UTC especificadas.
  • Largo/Corto: Solo largos.
  • Criterios de salida: Salir fuera de las horas especificadas.
  • Stops: No.
  • Valores predeterminados:
    • HoursLong = [0, 1, 2, 3]
    • MinTradeUsd = 200
    • CandleType = TimeSpan.FromHours(1)
  • Filtros:
    • Categoría: Estacionalidad
    • Dirección: Solo largos
    • Indicadores: Ninguno
    • Stops: No
    • Complejidad: Básico
    • Marco temporal: Intradía (1h)
    • Estacionalidad: Sí
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio
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;
}