Bitcoin 1H/15M Breakout
交易规则
使用已完成的 15 分钟足交易前一完整小时区间的突破。低周期足聚合至 UTC 小时桶;只有从精确小时起点开始的四根连续季度足全部存在,该小时才有效。初始不完整小时或数据缺口不能生成区间。
对开盘位于 H 小时的低周期足,只使用已完成 H−1 小时的最高/最低价,不使用正在形成的当前小时。空仓时,收盘严格高于该最高价买入 Volume,严格低于最低价卖出 Volume。允许双向,已有仓位不因突破而加仓或反转;活动订单阻止下一单。
原生本地保护将 StopLossBuffer 作为距实际入场成交的绝对价差,默认 50。止盈距离为 StopLossBuffer * RiskRewardRatio,默认比率 2 对应距离 100。两者均市价全仓退出,之后小时区间变化不移动保护水平。HigherTimeFrame 必须是低周期的整数倍。
本地原生保护使用该品种的蜡烛和已订阅价格更新,距离以实际入场成交为基准。退出为市价订单,成交价受可用市场及价格步长影响。
参数
| 参数 | 默认值 |
|---|---|
CandleType |
15m |
HigherTimeFrame |
1h |
StopLossBuffer |
50 |
RiskRewardRatio |
2 |
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 lower-timeframe closes outside the preceding complete higher-timeframe range.</summary>
public class Bitcoin1H15MBreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<TimeSpan> _higherTimeFrame;
private readonly StrategyParam<decimal> _stopLossBuffer;
private readonly StrategyParam<decimal> _riskRewardRatio;
private DateTime _bucket, _lastTime;
private decimal _high, _low;
private decimal? _previousHigh, _previousLow;
private int _count;
private Order _pending;
/// <summary>Finished lower timeframe candles.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>Completed higher timeframe range.</summary>
public TimeSpan HigherTimeFrame
{
get => _higherTimeFrame.Value;
set => _higherTimeFrame.Value = value;
}
/// <summary>Absolute stop distance from actual entry fills.</summary>
public decimal StopLossBuffer
{
get => _stopLossBuffer.Value;
set => _stopLossBuffer.Value = value;
}
/// <summary>Profit distance divided by stop distance.</summary>
public decimal RiskRewardRatio
{
get => _riskRewardRatio.Value;
set => _riskRewardRatio.Value = value;
}
/// <summary>Initializes the fifteen-minute/hourly breakout and fixed protection.</summary>
public Bitcoin1H15MBreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("CandleType", "Finished lower timeframe candles", "Parameters");
_higherTimeFrame = Param(nameof(HigherTimeFrame), TimeSpan.FromHours(1))
.SetDisplay("HigherTimeFrame", "Completed higher timeframe range", "Parameters");
_stopLossBuffer = Param(nameof(StopLossBuffer), 50m).SetGreaterThanZero()
.SetDisplay("StopLossBuffer", "Absolute stop distance from actual entry fills", "Parameters");
_riskRewardRatio = Param(nameof(RiskRewardRatio), 2m).SetGreaterThanZero()
.SetDisplay("RiskRewardRatio", "Profit distance divided by stop distance", "Parameters");
OrderRegistering += order => _pending = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bucket = _lastTime = default;
_high = _low = 0m;
_previousHigh = _previousLow = null;
_count = 0;
_pending = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
var frame = CandleType.Arg is TimeSpan interval ? interval : TimeSpan.Zero;
if (frame <= TimeSpan.Zero || HigherTimeFrame <= frame || HigherTimeFrame.Ticks % frame.Ticks != 0)
throw new InvalidOperationException("HigherTimeFrame must be an integer multiple of the lower time frame.");
base.OnStarted2(time);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Start();
}
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;
var frame = (TimeSpan)CandleType.Arg;
var bucket = new DateTime(candle.OpenTime.Ticks / HigherTimeFrame.Ticks * HigherTimeFrame.Ticks, DateTimeKind.Utc);
if (bucket != _bucket)
{
var complete = _bucket != default && bucket == _bucket + HigherTimeFrame
&& _count == HigherTimeFrame.Ticks / frame.Ticks && _lastTime + frame == bucket;
_previousHigh = complete ? _high : null;
_previousLow = complete ? _low : null;
_bucket = bucket;
_high = candle.HighPrice;
_low = candle.LowPrice;
_count = candle.OpenTime == bucket ? 1 : 0;
}
else
{
_count = _count > 0 && candle.OpenTime == _lastTime + frame ? _count + 1 : 0;
_high = Math.Max(_high, candle.HighPrice);
_low = Math.Min(_low, candle.LowPrice);
}
_lastTime = candle.OpenTime;
if (_previousHigh is null || _previousLow is null || Position != 0m || !IsFormedAndOnlineAndAllowTrading()
|| _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed))
return;
var side = candle.ClosePrice > _previousHigh ? Sides.Buy : candle.ClosePrice < _previousLow ? Sides.Sell : (Sides?)null;
if (side is null) return;
StartProtection(new Unit(StopLossBuffer * RiskRewardRatio, UnitTypes.Absolute),
new Unit(StopLossBuffer, UnitTypes.Absolute), useMarketOrders: true, isLocalStop: true);
if (side == Sides.Buy) BuyMarket(Volume);
else SellMarket(Volume);
}
}
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, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class bitcoin_1h15m_breakout_strategy(Strategy):
def __init__(self):
super(bitcoin_1h15m_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay(
"CandleType", "Finished lower timeframe candles", "Parameters")
self._higher_time_frame = self.Param("HigherTimeFrame", TimeSpan.FromHours(1)).SetDisplay(
"HigherTimeFrame", "Completed higher timeframe range", "Parameters")
self._stop_loss_buffer = self.Param("StopLossBuffer", 50.0).SetGreaterThanZero().SetDisplay(
"StopLossBuffer", "Absolute stop distance from actual entry fills", "Parameters")
self._risk_reward_ratio = self.Param("RiskRewardRatio", 2.0).SetGreaterThanZero().SetDisplay(
"RiskRewardRatio", "Profit distance divided by stop distance", "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 HigherTimeFrame(self):
return self._higher_time_frame.Value
@HigherTimeFrame.setter
def HigherTimeFrame(self, value):
self._higher_time_frame.Value = value
@property
def StopLossBuffer(self):
return self._stop_loss_buffer.Value
@StopLossBuffer.setter
def StopLossBuffer(self, value):
self._stop_loss_buffer.Value = value
@property
def RiskRewardRatio(self):
return self._risk_reward_ratio.Value
@RiskRewardRatio.setter
def RiskRewardRatio(self, value):
self._risk_reward_ratio.Value = value
def _reset_state(self):
self._bucket = self._last_time = DateTime.MinValue
self._high = self._low = Decimal(0)
self._previous_high = self._previous_low = None
self._count = 0
self._pending = None
def _remember_order(self, order):
self._pending = order
def OnReseted(self):
super(bitcoin_1h15m_breakout_strategy, self).OnReseted()
self._reset_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
frame = self.CandleType.Arg
if not isinstance(frame, TimeSpan) or frame <= TimeSpan.Zero or self.HigherTimeFrame <= frame or self.HigherTimeFrame.Ticks % frame.Ticks != 0:
raise ValueError("HigherTimeFrame must be an integer multiple of the lower time frame.")
super(bitcoin_1h15m_breakout_strategy, self).OnStarted2(time)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Start()
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
frame = self.CandleType.Arg
bucket = DateTime(candle.OpenTime.Ticks // self.HigherTimeFrame.Ticks * self.HigherTimeFrame.Ticks, DateTimeKind.Utc)
if bucket != self._bucket:
complete = self._bucket != DateTime.MinValue and bucket == self._bucket.Add(self.HigherTimeFrame) and self._count == self.HigherTimeFrame.Ticks // frame.Ticks and self._last_time.Add(frame) == bucket
self._previous_high = self._high if complete else None
self._previous_low = self._low if complete else None
self._bucket, self._high, self._low = bucket, candle.HighPrice, candle.LowPrice
self._count = 1 if candle.OpenTime == bucket else 0
else:
self._count = self._count + 1 if self._count > 0 and candle.OpenTime == self._last_time.Add(frame) else 0
self._high = max(self._high, candle.HighPrice)
self._low = min(self._low, candle.LowPrice)
self._last_time = candle.OpenTime
if self._previous_high is None or self._previous_low is None or self.Position != 0 or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
side = Sides.Buy if candle.ClosePrice > self._previous_high else Sides.Sell if candle.ClosePrice < self._previous_low else None
if side is None:
return
self.StartProtection(Unit(float(self.StopLossBuffer) * float(self.RiskRewardRatio), UnitTypes.Absolute),
Unit(float(self.StopLossBuffer), UnitTypes.Absolute), useMarketOrders=True, isLocalStop=True)
if side == Sides.Buy:
self.BuyMarket(self.Volume)
else:
self.SellMarket(self.Volume)
def CreateClone(self):
return bitcoin_1h15m_breakout_strategy()