Lunar Calendar Day Crypto Trading Strategy
Trades based on the lunar calendar. The strategy enters a long position on the 12th day of each lunar month and exits on the 26th.
Lunar calendar data for 2020–2026 is preloaded using the Asia/Seoul time zone.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades according to the lunar calendar.
/// Enters a long position on a specified lunar day and exits on another.
/// </summary>
public class LunarCalendarDayCryptoTradingStrategy : Strategy
{
private static readonly TimeSpan SeoulOffset = TimeSpan.FromHours(9);
private static readonly Dictionary<int, (DateTimeOffset Start, int[] Lengths)> LunarData = new()
{
{2020, (new DateTimeOffset(2020, 1, 25, 0, 0, 0, SeoulOffset), new[] {29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29})},
{2021, (new DateTimeOffset(2021, 2, 12, 0, 0, 0, SeoulOffset), new[] {30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30})},
{2022, (new DateTimeOffset(2022, 2, 1, 0, 0, 0, SeoulOffset), new[] {29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29})},
{2023, (new DateTimeOffset(2023, 1, 22, 0, 0, 0, SeoulOffset), new[] {30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30})},
{2024, (new DateTimeOffset(2024, 2, 10, 0, 0, 0, SeoulOffset), new[] {30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29})},
{2025, (new DateTimeOffset(2025, 1, 29, 0, 0, 0, SeoulOffset), new[] {29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29})},
{2026, (new DateTimeOffset(2026, 2, 17, 0, 0, 0, SeoulOffset), new[] {30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30})},
};
private readonly StrategyParam<int> _buyDay;
private readonly StrategyParam<int> _sellDay;
private readonly StrategyParam<DataType> _candleType;
private DateTimeOffset _lastTradeDate;
public int BuyDay { get => _buyDay.Value; set => _buyDay.Value = value; }
public int SellDay { get => _sellDay.Value; set => _sellDay.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public LunarCalendarDayCryptoTradingStrategy()
{
_buyDay = Param(nameof(BuyDay), 12)
.SetDisplay("Buy Day", "Lunar day to enter long", "Trading")
.SetOptimize(1, 30, 1);
_sellDay = Param(nameof(SellDay), 26)
.SetDisplay("Sell Day", "Lunar day to exit", "Trading")
.SetOptimize(1, 30, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastTradeDate = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_lastTradeDate = default;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var day = GetLunarDay(candle.OpenTime);
if (day is null)
return;
// Only trade once per calendar day
if (candle.OpenTime.Date == _lastTradeDate.Date)
return;
if (day == BuyDay && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_lastTradeDate = candle.OpenTime;
}
if (day == SellDay && Position > 0)
{
SellMarket();
_lastTradeDate = candle.OpenTime;
}
}
private static int? GetLunarDay(DateTimeOffset time)
{
if (!LunarData.TryGetValue(time.Year, out var data))
return null;
if (time < data.Start)
return null;
var days = (time.Date - data.Start.Date).Days;
var offset = 0;
foreach (var length in data.Lengths)
{
if (days < offset + length)
return days - offset + 1;
offset += length;
}
return null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, DateTimeOffset
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class lunar_calendar_day_crypto_trading_strategy(Strategy):
SEOUL_OFFSET_HOURS = 9
LUNAR_DATA = {
2020: ("2020-01-25", [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29]),
2021: ("2021-02-12", [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30]),
2022: ("2022-02-01", [29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29]),
2023: ("2023-01-22", [30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
2024: ("2024-02-10", [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29]),
2025: ("2025-01-29", [29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29]),
2026: ("2026-02-17", [30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
}
def __init__(self):
super(lunar_calendar_day_crypto_trading_strategy, self).__init__()
self._buy_day = self.Param("BuyDay", 12) \
.SetDisplay("Buy Day", "Lunar day to enter long", "Trading")
self._sell_day = self.Param("SellDay", 26) \
.SetDisplay("Sell Day", "Lunar day to exit", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for candles", "General")
self._last_trade_date = DateTimeOffset.MinValue
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(lunar_calendar_day_crypto_trading_strategy, self).OnReseted()
self._last_trade_date = DateTimeOffset.MinValue
def OnStarted2(self, time):
super(lunar_calendar_day_crypto_trading_strategy, self).OnStarted2(time)
self._last_trade_date = DateTimeOffset.MinValue
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _parse_date(self, date_str):
parts = date_str.split("-")
return int(parts[0]), int(parts[1]), int(parts[2])
def _get_lunar_day(self, open_time):
try:
year = open_time.Year
except:
return None
if year not in self.LUNAR_DATA:
return None
date_str, lengths = self.LUNAR_DATA[year]
sy, sm, sd = self._parse_date(date_str)
try:
start = DateTimeOffset(sy, sm, sd, 0, 0, 0, TimeSpan.FromHours(self.SEOUL_OFFSET_HOURS))
except:
return None
if open_time.Ticks < start.Ticks:
return None
days = (open_time.Date - start.Date).Days
offset = 0
for length in lengths:
if days < offset + length:
return days - offset + 1
offset += length
return None
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
day = self._get_lunar_day(candle.OpenTime)
if day is None:
return
if candle.OpenTime.Date == self._last_trade_date.Date:
return
if day == self._buy_day.Value and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._last_trade_date = candle.OpenTime
if day == self._sell_day.Value and self.Position > 0:
self.SellMarket()
self._last_trade_date = candle.OpenTime
def CreateClone(self):
return lunar_calendar_day_crypto_trading_strategy()