BTCUSD Momentum After Abnormal Days
取引ルール
確定日足のリターンは r=(Close-Open)/Open です。現在を除く直前 LookbackPeriod 本の確定日次リターンで平均と母標準偏差を計算し、前窓全体の形成を要求します。r>mean+k*sd で買い、r<mean-k*sd で売り、k=AbnormalThreshold。閾値と等しい場合は入りません。
新規はフラット時のみです。数量は floor(CapitalPerTrade/Close/VolumeStep)*VolumeStep。最小未満は見送り、最大超過は拒否します。資金額はシグナル終値基準で、約定価格は異なる場合があります。約定済みの全数量を次の日足終値、欠損時には最初の利用可能な後続日足終値で決済します。同じ足では決済を優先し再入場しません。未完了注文は重複を防ぎ、価格ストップは設定しません。
パラメーター
| パラメーター | 既定値 |
|---|---|
CandleType |
1d |
LookbackPeriod |
5 |
AbnormalThreshold |
1.6 |
CapitalPerTrade |
1000 |
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Trades a completed daily return anomaly and exits at the next available daily close.</summary>
public class BtcusdMomentumAfterAbnormalDaysStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _abnormalThreshold;
private readonly StrategyParam<decimal> _capitalPerTrade;
private readonly Queue<decimal> _returns = [];
private DateTime _lastDate;
private DateTime? _entryDay;
private Order _pending;
/// <summary>Finished daily signal candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>Prior completed daily returns in the baseline.</summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>Population standard deviation multiplier.</summary>
public decimal AbnormalThreshold
{
get => _abnormalThreshold.Value;
set => _abnormalThreshold.Value = value;
}
/// <summary>Signal-close quote currency budget.</summary>
public decimal CapitalPerTrade
{
get => _capitalPerTrade.Value;
set => _capitalPerTrade.Value = value;
}
/// <summary>Initializes the daily anomaly and capital parameters.</summary>
public BtcusdMomentumAfterAbnormalDaysStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("CandleType", "Finished daily signal candle interval", "Parameters");
_lookbackPeriod = Param(nameof(LookbackPeriod), 5).SetGreaterThanZero()
.SetDisplay("LookbackPeriod", "Prior completed daily returns in the baseline", "Parameters");
_abnormalThreshold = Param(nameof(AbnormalThreshold), 1.6m).SetGreaterThanZero()
.SetDisplay("AbnormalThreshold", "Population standard deviation multiplier", "Parameters");
_capitalPerTrade = Param(nameof(CapitalPerTrade), 1000m).SetGreaterThanZero()
.SetDisplay("CapitalPerTrade", "Signal-close quote currency budget", "Parameters");
OrderRegistering += order => _pending = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_returns.Clear();
_lastDate = default;
_entryDay = null;
_pending = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (!CandleType.Equals(TimeSpan.FromDays(1).TimeFrame()))
throw new InvalidOperationException("Daily candles are required for daily anomaly and exit deadlines.");
base.OnStarted2(time);
var candles = SubscribeCandles(CandleType);
candles.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || candle.OpenPrice <= 0m || candle.ClosePrice <= 0m || candle.OpenTime.Date <= _lastDate)
return;
_lastDate = candle.OpenTime.Date;
var sample = _returns.ToArray();
var value = (candle.ClosePrice - candle.OpenPrice) / candle.OpenPrice;
_returns.Enqueue(value);
while (_returns.Count > LookbackPeriod) _returns.Dequeue();
if (!IsFormedAndOnlineAndAllowTrading() || _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position != 0m)
{
if (_entryDay is DateTime day && candle.OpenTime.Date > day)
{
if (Position > 0m) SellMarket(Position);
else BuyMarket(-Position);
}
return;
}
if (sample.Length < LookbackPeriod) return;
var mean = sample.Average();
var deviation = (decimal)Math.Sqrt((double)sample.Average(r => (r - mean) * (r - mean)));
var side = value > mean + AbnormalThreshold * deviation ? Sides.Buy
: value < mean - AbnormalThreshold * deviation ? Sides.Sell : (Sides?)null;
if (side is null) return;
var step = Security.VolumeStep ?? 1m;
var quantity = Math.Floor(CapitalPerTrade / candle.ClosePrice / step) * step;
if (quantity < (Security.MinVolume ?? step)) return;
if (Security.MaxVolume is decimal maximum && quantity > maximum)
throw new InvalidOperationException("CapitalPerTrade exceeds the instrument's maximum order volume.");
_entryDay = candle.OpenTime.Date;
if (side == Sides.Buy) BuyMarket(quantity);
else SellMarket(quantity);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
import math
from collections import deque
from System import TimeSpan, DateTime, DateTimeKind, Decimal, Nullable
from StockSharp.Messages import DataType, CandleStates, Sides, OrderStates, Unit, UnitTypes
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class btcusd_momentum_after_abnormal_days_strategy(Strategy):
def __init__(self):
super(btcusd_momentum_after_abnormal_days_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))).SetDisplay(
"CandleType", "Finished daily signal candle interval", "Parameters")
self._lookback_period = self.Param("LookbackPeriod", 5).SetGreaterThanZero().SetDisplay(
"LookbackPeriod", "Prior completed daily returns in the baseline", "Parameters")
self._abnormal_threshold = self.Param("AbnormalThreshold", 1.6).SetGreaterThanZero().SetDisplay(
"AbnormalThreshold", "Population standard deviation multiplier", "Parameters")
self._capital_per_trade = self.Param("CapitalPerTrade", 1000.0).SetGreaterThanZero().SetDisplay(
"CapitalPerTrade", "Signal-close quote currency budget", "Parameters")
self._reset_state()
self.OrderRegistering += self._remember_order
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@LookbackPeriod.setter
def LookbackPeriod(self, value):
self._lookback_period.Value = value
@property
def AbnormalThreshold(self):
return self._abnormal_threshold.Value
@AbnormalThreshold.setter
def AbnormalThreshold(self, value):
self._abnormal_threshold.Value = value
@property
def CapitalPerTrade(self):
return self._capital_per_trade.Value
@CapitalPerTrade.setter
def CapitalPerTrade(self, value):
self._capital_per_trade.Value = value
def _reset_state(self):
self._returns = deque()
self._last_date = DateTime.MinValue
self._entry_day = None
self._pending = None
def _remember_order(self, order):
self._pending = order
def OnReseted(self):
super(btcusd_momentum_after_abnormal_days_strategy, self).OnReseted()
self._reset_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnStarted2(self, time):
if self.CandleType != DataType.TimeFrame(TimeSpan.FromDays(1)):
raise ValueError("Daily candles are required for daily anomaly and exit deadlines.")
super(btcusd_momentum_after_abnormal_days_strategy, self).OnStarted2(time)
candles = self.SubscribeCandles(self.CandleType)
candles.Bind(self._process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawOwnTrades(area)
def _process(self, candle):
if candle.State != CandleStates.Finished or candle.OpenPrice <= 0 or candle.ClosePrice <= 0 or candle.OpenTime.Date <= self._last_date:
return
self._last_date = candle.OpenTime.Date
sample = list(self._returns)
value = float((candle.ClosePrice - candle.OpenPrice) / candle.OpenPrice)
self._returns.append(value)
while len(self._returns) > self.LookbackPeriod:
self._returns.popleft()
if not self.IsFormedAndOnlineAndAllowTrading() or (self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position != 0:
if self._entry_day is not None and candle.OpenTime.Date > self._entry_day:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(-self.Position)
return
if len(sample) < self.LookbackPeriod:
return
mean = sum(sample) / len(sample)
deviation = math.sqrt(sum((r - mean) ** 2 for r in sample) / len(sample))
side = Sides.Buy if value > mean + float(self.AbnormalThreshold) * deviation else Sides.Sell if value < mean - float(self.AbnormalThreshold) * deviation else None
if side is None:
return
step = float(self.Security.VolumeStep or 1)
quantity = math.floor(float(self.CapitalPerTrade) / float(candle.ClosePrice) / step) * step
if quantity < float(self.Security.MinVolume or step):
return
if self.Security.MaxVolume is not None and quantity > float(self.Security.MaxVolume):
raise ValueError("CapitalPerTrade exceeds the instrument's maximum order volume.")
self._entry_day = candle.OpenTime.Date
if side == Sides.Buy:
self.BuyMarket(Decimal(quantity))
else:
self.SellMarket(Decimal(quantity))
def CreateClone(self):
return btcusd_momentum_after_abnormal_days_strategy()