BTC Difficulty Adjustments
取引ルール
表頭が Time,Difficulty の DifficultyHistoryFile が必須です。公開時刻は厳密に増加する Z 付き ISO UTC 時刻、難易度は正である必要があります。起動時に読み込み、欠落や不正形式は明示的に失敗します。市場価格から採掘難易度を推定しません。
確定足では OpenTime 以前に公開された観測だけを使用します。最初の値は基準となり、変化なしではシグナルを出しません。変化率は 100*(difficulty/previous-1)。閾値モード無効時は上昇で買い、低下で売りです。有効時は abs(change)>=Threshold のときのみ、低下で買い、上昇で売ります。条件を満たす最新の既知の変化で方向を決定します。新規は Volume、反転は Volume+abs(Position) です。未完了注文は判断の重複を防ぎ、自動ストップはありません。
パラメーター
| パラメーター | 既定値 |
|---|---|
CandleType |
1d |
ThresholdMode |
false |
Threshold |
10 |
DifficultyHistoryFile |
必須 |
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;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Trades published Bitcoin difficulty adjustments without substituting price momentum.</summary>
public class BtcDifficultyAdjustmentsStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _thresholdMode;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<string> _difficultyHistoryFile;
private readonly List<(DateTime time, decimal difficulty)> _observations = [];
private int _cursor;
private decimal? _previous;
private DateTime _lastTime;
private Order _pending;
/// <summary>Finished decision candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>Use contrarian thresholded adjustments.</summary>
public bool ThresholdMode
{
get => _thresholdMode.Value;
set => _thresholdMode.Value = value;
}
/// <summary>Minimum absolute difficulty change in percent.</summary>
public decimal Threshold
{
get => _threshold.Value;
set => _threshold.Value = value;
}
/// <summary>Required Time,Difficulty CSV with UTC publication timestamps.</summary>
public string DifficultyHistoryFile
{
get => _difficultyHistoryFile.Value;
set => _difficultyHistoryFile.Value = value;
}
/// <summary>Initializes the published difficulty strategy parameters.</summary>
public BtcDifficultyAdjustmentsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("CandleType", "Finished decision candle interval", "Parameters");
_thresholdMode = Param(nameof(ThresholdMode), false)
.SetDisplay("ThresholdMode", "Use contrarian thresholded adjustments", "Parameters");
_threshold = Param(nameof(Threshold), 10m).SetGreaterThanZero()
.SetDisplay("Threshold", "Minimum absolute difficulty change in percent", "Parameters");
_difficultyHistoryFile = Param(nameof(DifficultyHistoryFile), string.Empty)
.SetDisplay("DifficultyHistoryFile", "Required Time,Difficulty CSV with UTC publication timestamps", "Parameters");
OrderRegistering += order => _pending = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_observations.Clear();
_cursor = 0;
_previous = null;
_lastTime = default;
_pending = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (string.IsNullOrWhiteSpace(DifficultyHistoryFile) || !File.Exists(DifficultyHistoryFile))
throw new InvalidOperationException("DifficultyHistoryFile must identify an existing Time,Difficulty CSV.");
_observations.Clear();
var lines = File.ReadAllLines(DifficultyHistoryFile);
if (lines.Length < 3 || lines[0].Trim() != "Time,Difficulty")
throw new FormatException("DifficultyHistoryFile requires Time,Difficulty and at least two observations.");
foreach (var line in lines.Skip(1).Where(l => !string.IsNullOrWhiteSpace(l)))
{
var cells = line.Split(',');
if (cells.Length != 2 || !cells[0].Trim().EndsWith("Z", StringComparison.OrdinalIgnoreCase))
throw new FormatException("DifficultyHistoryFile timestamps must be ISO UTC values ending in Z.");
var stamp = DateTime.Parse(cells[0], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
var value = decimal.Parse(cells[1], CultureInfo.InvariantCulture);
if (value <= 0m || _observations.Count > 0 && stamp <= _observations[^1].time)
throw new FormatException("DifficultyHistoryFile requires positive values and strictly increasing timestamps.");
_observations.Add((stamp, value));
}
if (_observations.Count < 2)
throw new FormatException("DifficultyHistoryFile requires at least two observations.");
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.OpenTime <= _lastTime)
return;
_lastTime = candle.OpenTime;
Sides? signal = null;
while (_cursor < _observations.Count && _observations[_cursor].time <= candle.OpenTime)
{
var value = _observations[_cursor++].difficulty;
if (_previous is decimal prior && value != prior)
{
var change = (value / prior - 1m) * 100m;
signal = !ThresholdMode || Math.Abs(change) >= Threshold
? ThresholdMode ? change < 0m ? Sides.Buy : Sides.Sell : change > 0m ? Sides.Buy : Sides.Sell
: null;
}
_previous = value;
}
if (signal is null || !IsFormedAndOnlineAndAllowTrading()
|| _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (signal == Sides.Buy && Position <= 0m)
BuyMarket(Volume + Math.Abs(Position));
else if (signal == Sides.Sell && Position >= 0m)
SellMarket(Volume + Math.Abs(Position));
}
}
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
from System.IO import File
from System.Globalization import CultureInfo, DateTimeStyles
class btc_difficulty_adjustments_strategy(Strategy):
def __init__(self):
super(btc_difficulty_adjustments_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))).SetDisplay(
"CandleType", "Finished decision candle interval", "Parameters")
self._threshold_mode = self.Param("ThresholdMode", False).SetDisplay(
"ThresholdMode", "Use contrarian thresholded adjustments", "Parameters")
self._threshold = self.Param("Threshold", 10.0).SetGreaterThanZero().SetDisplay(
"Threshold", "Minimum absolute difficulty change in percent", "Parameters")
self._difficulty_history_file = self.Param("DifficultyHistoryFile", "").SetDisplay(
"DifficultyHistoryFile", "Required Time,Difficulty CSV with UTC publication timestamps", "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 ThresholdMode(self):
return self._threshold_mode.Value
@ThresholdMode.setter
def ThresholdMode(self, value):
self._threshold_mode.Value = value
@property
def Threshold(self):
return self._threshold.Value
@Threshold.setter
def Threshold(self, value):
self._threshold.Value = value
@property
def DifficultyHistoryFile(self):
return self._difficulty_history_file.Value
@DifficultyHistoryFile.setter
def DifficultyHistoryFile(self, value):
self._difficulty_history_file.Value = value
def _reset_state(self):
self._observations = []
self._cursor = 0
self._previous = None
self._last_time = DateTime.MinValue
self._pending = None
def _remember_order(self, order):
self._pending = order
def OnReseted(self):
super(btc_difficulty_adjustments_strategy, self).OnReseted()
self._reset_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnStarted2(self, time):
if not self.DifficultyHistoryFile or not File.Exists(self.DifficultyHistoryFile):
raise ValueError("DifficultyHistoryFile must identify an existing Time,Difficulty CSV.")
lines = list(File.ReadAllLines(self.DifficultyHistoryFile))
if len(lines) < 3 or lines[0].strip() != "Time,Difficulty":
raise ValueError("DifficultyHistoryFile requires Time,Difficulty and at least two observations.")
self._observations = []
for line in lines[1:]:
if not line.strip():
continue
cells = line.split(",")
if len(cells) != 2 or not cells[0].strip().upper().endswith("Z"):
raise ValueError("DifficultyHistoryFile timestamps must be ISO UTC values ending in Z.")
stamp = DateTime.Parse(cells[0], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)
value = Decimal.Parse(cells[1], CultureInfo.InvariantCulture)
if value <= 0 or (self._observations and stamp <= self._observations[-1][0]):
raise ValueError("DifficultyHistoryFile requires positive values and strictly increasing timestamps.")
self._observations.append((stamp, value))
if len(self._observations) < 2:
raise ValueError("DifficultyHistoryFile requires at least two observations.")
super(btc_difficulty_adjustments_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.OpenTime <= self._last_time:
return
self._last_time = candle.OpenTime
side = None
while self._cursor < len(self._observations) and self._observations[self._cursor][0] <= candle.OpenTime:
value = self._observations[self._cursor][1]
self._cursor += 1
if self._previous is not None and value != self._previous:
change = (value / self._previous - Decimal(1)) * Decimal(100)
if not self.ThresholdMode or abs(change) >= self.Threshold:
side = (Sides.Buy if change < 0 else Sides.Sell) if self.ThresholdMode else (Sides.Buy if change > 0 else Sides.Sell)
else:
side = None
self._previous = value
if side is None or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
if side == Sides.Buy and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif side == Sides.Sell and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def CreateClone(self):
return btc_difficulty_adjustments_strategy()