BTC DCA AHR999
交易规则
使用已完成的 UTC 日线。以最近 Length 个正收盘价(含当前)计算 G=exp(mean(ln(C)))。BTC 年龄为自 2009-01-03 起的整天数;F=10^(5.84*log10(age)-17.01),AHR999=(C/G)*(C/F)。窗口必须完整。
在日期范围内的星期一,指数严格低于 0.45 时按 UsdInvest2 预算买入,否则严格低于 1.2 时使用 UsdInvest1。其余指数、星期及日期不买入。UTC 日期边界包含在内,每根日线最多决策一次。数量为 floor(budget/C/VolumeStep)*VolumeStep,不足最小数量则跳过。预算按信号收盘价计算,实际市价成交可能不同。未完成订单阻止重复提交。持仓累积,没有自动退出。
参数
| 参数 | 默认值 |
|---|---|
CandleType |
1d |
Length |
200 |
UsdInvest1 |
100 |
UsdInvest2 |
1000 |
StartDate |
2024-02-01T00:00:00Z |
EndDate |
2025-12-31T00:00:00Z |
参考资料
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>Daily geometric AHR999 accumulation with one budgeted purchase per eligible UTC Monday.</summary>
public class BtcDcaAhr999Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _usdInvest1;
private readonly StrategyParam<decimal> _usdInvest2;
private readonly StrategyParam<DateTime> _startDate;
private readonly StrategyParam<DateTime> _endDate;
private readonly Queue<decimal> _prices = [];
private DateTime _lastDate;
private Order _pending;
/// <summary>Finished daily candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>Daily geometric mean window.</summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>Budget below AHR999 1.2.</summary>
public decimal UsdInvest1
{
get => _usdInvest1.Value;
set => _usdInvest1.Value = value;
}
/// <summary>Budget below AHR999 0.45.</summary>
public decimal UsdInvest2
{
get => _usdInvest2.Value;
set => _usdInvest2.Value = value;
}
/// <summary>First eligible UTC date.</summary>
public DateTime StartDate
{
get => _startDate.Value;
set => _startDate.Value = value;
}
/// <summary>Last eligible UTC date.</summary>
public DateTime EndDate
{
get => _endDate.Value;
set => _endDate.Value = value;
}
/// <summary>Latest fully formed AHR999 observation.</summary>
public decimal? LastAhr999 { get; private set; }
/// <summary>Initializes the published accumulation parameters.</summary>
public BtcDcaAhr999Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("CandleType", "Finished daily candle interval", "Parameters");
_length = Param(nameof(Length), 200).SetGreaterThanZero()
.SetDisplay("Length", "Daily geometric mean window", "Parameters");
_usdInvest1 = Param(nameof(UsdInvest1), 100m).SetGreaterThanZero()
.SetDisplay("UsdInvest1", "Budget below AHR999 1.2", "Parameters");
_usdInvest2 = Param(nameof(UsdInvest2), 1000m).SetGreaterThanZero()
.SetDisplay("UsdInvest2", "Budget below AHR999 0.45", "Parameters");
_startDate = Param(nameof(StartDate), new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc))
.SetDisplay("StartDate", "First eligible UTC date", "Parameters");
_endDate = Param(nameof(EndDate), new DateTime(2025, 12, 31, 0, 0, 0, DateTimeKind.Utc))
.SetDisplay("EndDate", "Last eligible UTC date", "Parameters");
OrderRegistering += order => _pending = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prices.Clear();
_lastDate = default;
_pending = null;
LastAhr999 = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (!CandleType.Equals(TimeSpan.FromDays(1).TimeFrame()) || StartDate.Kind != DateTimeKind.Utc
|| EndDate.Kind != DateTimeKind.Utc || StartDate > EndDate)
throw new InvalidOperationException("Daily candles and an ordered UTC date range are required.");
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.ClosePrice <= 0m || candle.OpenTime.Date <= _lastDate)
return;
var date = candle.OpenTime.Date;
_lastDate = date;
_prices.Enqueue(candle.ClosePrice);
while (_prices.Count > Length) _prices.Dequeue();
if (_prices.Count < Length) return;
var age = (date - new DateTime(2009, 1, 3, 0, 0, 0, DateTimeKind.Utc)).TotalDays;
if (age <= 0) return;
var geometric = Math.Exp(_prices.Average(p => Math.Log((double)p)));
var fitted = Math.Pow(10, 5.84 * Math.Log10(age) - 17.01);
LastAhr999 = (decimal)((double)candle.ClosePrice / geometric * (double)candle.ClosePrice / fitted);
if (!IsFormedAndOnlineAndAllowTrading() || date < StartDate.Date || date > EndDate.Date || date.DayOfWeek != DayOfWeek.Monday
|| _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed))
return;
var budget = LastAhr999 < 0.45m ? UsdInvest2 : LastAhr999 < 1.2m ? UsdInvest1 : 0m;
if (budget == 0m) return;
var step = Security.VolumeStep ?? 1m;
var quantity = Math.Floor(budget / candle.ClosePrice / step) * step;
if (quantity < (Security.MinVolume ?? step)) return;
if (Security.MaxVolume is decimal maximum && quantity > maximum)
throw new InvalidOperationException("The investment exceeds the instrument's maximum order volume.");
BuyMarket(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
from System import DayOfWeek
class btc_dca_ahr999_strategy(Strategy):
def __init__(self):
super(btc_dca_ahr999_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))).SetDisplay(
"CandleType", "Finished daily candle interval", "Parameters")
self._length = self.Param("Length", 200).SetGreaterThanZero().SetDisplay(
"Length", "Daily geometric mean window", "Parameters")
self._usd_invest1 = self.Param("UsdInvest1", 100.0).SetGreaterThanZero().SetDisplay(
"UsdInvest1", "Budget below AHR999 1.2", "Parameters")
self._usd_invest2 = self.Param("UsdInvest2", 1000.0).SetGreaterThanZero().SetDisplay(
"UsdInvest2", "Budget below AHR999 0.45", "Parameters")
self._start_date = self.Param("StartDate", DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc)).SetDisplay(
"StartDate", "First eligible UTC date", "Parameters")
self._end_date = self.Param("EndDate", DateTime(2025, 12, 31, 0, 0, 0, DateTimeKind.Utc)).SetDisplay(
"EndDate", "Last eligible UTC date", "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 Length(self):
return self._length.Value
@Length.setter
def Length(self, value):
self._length.Value = value
@property
def UsdInvest1(self):
return self._usd_invest1.Value
@UsdInvest1.setter
def UsdInvest1(self, value):
self._usd_invest1.Value = value
@property
def UsdInvest2(self):
return self._usd_invest2.Value
@UsdInvest2.setter
def UsdInvest2(self, value):
self._usd_invest2.Value = value
@property
def StartDate(self):
return self._start_date.Value
@StartDate.setter
def StartDate(self, value):
self._start_date.Value = value
@property
def EndDate(self):
return self._end_date.Value
@EndDate.setter
def EndDate(self, value):
self._end_date.Value = value
@property
def LastAhr999(self):
return self._last_ahr
def _reset_state(self):
self._prices = deque()
self._last_date = DateTime.MinValue
self._pending = None
self._last_ahr = None
def _remember_order(self, order):
self._pending = order
def OnReseted(self):
super(btc_dca_ahr999_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)) or self.StartDate.Kind != DateTimeKind.Utc or self.EndDate.Kind != DateTimeKind.Utc or self.StartDate > self.EndDate:
raise ValueError("Daily candles and an ordered UTC date range are required.")
super(btc_dca_ahr999_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.ClosePrice <= 0 or candle.OpenTime.Date <= self._last_date:
return
date = candle.OpenTime.Date
self._last_date = date
close = float(candle.ClosePrice)
self._prices.append(close)
while len(self._prices) > self.Length:
self._prices.popleft()
if len(self._prices) < self.Length:
return
age = (date - DateTime(2009, 1, 3, 0, 0, 0, DateTimeKind.Utc)).TotalDays
if age <= 0:
return
geometric = math.exp(sum(math.log(p) for p in self._prices) / self.Length)
fitted = 10.0 ** (5.84 * math.log10(age) - 17.01)
self._last_ahr = close / geometric * close / fitted
if not self.IsFormedAndOnlineAndAllowTrading() or date < self.StartDate.Date or date > self.EndDate.Date or date.DayOfWeek != DayOfWeek.Monday:
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
budget = float(self.UsdInvest2) if self._last_ahr < 0.45 else float(self.UsdInvest1) if self._last_ahr < 1.2 else 0.0
if budget == 0:
return
step = float(self.Security.VolumeStep or 1)
quantity = math.floor(budget / close / 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("The investment exceeds the instrument's maximum order volume.")
self.BuyMarket(Decimal(quantity))
def CreateClone(self):
return btc_dca_ahr999_strategy()