ChopFlow ATRスキャルプ
ChopFlow ATR Scalpは市場がチョッピーな状態から抜け出し、OBVがそのEMAを突破した時にエントリーします。エグジットはATRベースの対称的なストップとターゲットを使用します。
目的はトレンド形成の初期における素早い動きを捉えることです。
詳細
- エントリー条件:
Choppiness < ChopThresholdかつ OBVがそのEMAの上/下にある。 - ロング/ショート: 両方向。
- エグジット条件: ATRストップまたはテイクプロフィット距離。
- ストップ: はい。
- デフォルト値:
AtrLength= 14AtrMultiplier= 1.5ChopLength= 14ChopThreshold= 60ObvEmaLength= 10SessionInput= "1700-1600"CandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: スキャルピング
- 方向: 両方
- インジケーター: ATR, Choppiness, OBV
- ストップ: はい
- 複雑さ: 基本
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// ChopFlow ATR Scalp strategy.
/// Inside the trading session, when the Choppiness Index is below ChopThreshold the market is trending: a flat position goes long
/// with OBV above its EMA and short with OBV below it. The exit is a symmetric stop and target placed AtrMultiplier ATRs from the entry.
/// </summary>
public class ChopFlowAtrScalpStrategy : Strategy
{
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _chopLength;
private readonly StrategyParam<decimal> _chopThreshold;
private readonly StrategyParam<int> _obvEmaLength;
private readonly StrategyParam<string> _sessionInput;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<(decimal tr, decimal high, decimal low)> _chopBars = new();
private ExponentialMovingAverage _obvEma;
private decimal? _prevClose;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// ATR multiple for the stop and the target.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Choppiness Index period.
/// </summary>
public int ChopLength
{
get => _chopLength.Value;
set => _chopLength.Value = value;
}
/// <summary>
/// Choppiness level below which entries are allowed.
/// </summary>
public decimal ChopThreshold
{
get => _chopThreshold.Value;
set => _chopThreshold.Value = value;
}
/// <summary>
/// EMA period applied to OBV.
/// </summary>
public int ObvEmaLength
{
get => _obvEmaLength.Value;
set => _obvEmaLength.Value = value;
}
/// <summary>
/// Trading session as HHMM-HHMM in UTC.
/// </summary>
public string SessionInput
{
get => _sessionInput.Value;
set => _sessionInput.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ChopFlowAtrScalpStrategy()
{
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "ATR multiple for the stop and the target", "Risk");
_chopLength = Param(nameof(ChopLength), 14)
.SetRange(2, 1000)
.SetDisplay("Chop Length", "Choppiness Index period", "Indicators");
_chopThreshold = Param(nameof(ChopThreshold), 60m)
.SetDisplay("Chop Threshold", "Choppiness level below which entries are allowed", "Indicators");
_obvEmaLength = Param(nameof(ObvEmaLength), 10)
.SetGreaterThanZero()
.SetDisplay("OBV EMA Length", "EMA period applied to OBV", "Indicators");
_sessionInput = Param(nameof(SessionInput), "1700-1600")
.SetDisplay("Session", "Trading session as HHMM-HHMM in UTC", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
_obvEma = null;
}
private void ResetState()
{
_chopBars.Clear();
_prevClose = null;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var atr = new AverageTrueRange { Length = AtrLength };
var obv = new OnBalanceVolume();
_obvEma = new ExponentialMovingAverage { Length = ObvEmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(atr, obv, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var volumeArea = CreateChartArea();
if (volumeArea != null)
{
DrawIndicator(volumeArea, obv);
DrawIndicator(volumeArea, _obvEma);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue, IIndicatorValue obvValue)
{
if (candle.State != CandleStates.Finished)
return;
var chop = UpdateChoppiness(candle);
if (!obvValue.IsFormed)
return;
var obv = obvValue.ToDecimal();
var obvEmaValue = _obvEma.Process(obv, candle.OpenTime, true);
if (Position > 0 && _stopPrice is decimal longStop && _takePrice is decimal longTake)
{
if (candle.LowPrice <= longStop || candle.HighPrice >= longTake)
{
SellMarket(Position);
_stopPrice = null;
_takePrice = null;
}
return;
}
if (Position < 0 && _stopPrice is decimal shortStop && _takePrice is decimal shortTake)
{
if (candle.HighPrice >= shortStop || candle.LowPrice <= shortTake)
{
BuyMarket(-Position);
_stopPrice = null;
_takePrice = null;
}
return;
}
if (Position != 0 || !atrValue.IsFormed || !obvEmaValue.IsFormed || chop is not decimal chopValue)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (chopValue >= ChopThreshold || !InSession(candle.OpenTime))
return;
var obvEma = obvEmaValue.ToDecimal();
var distance = atrValue.ToDecimal() * AtrMultiplier;
var close = candle.ClosePrice;
if (obv > obvEma)
{
BuyMarket(Volume);
_stopPrice = close - distance;
_takePrice = close + distance;
}
else if (obv < obvEma)
{
SellMarket(Volume);
_stopPrice = close + distance;
_takePrice = close - distance;
}
}
private decimal? UpdateChoppiness(ICandleMessage candle)
{
var tr = _prevClose is decimal prev
? Math.Max(candle.HighPrice, prev) - Math.Min(candle.LowPrice, prev)
: candle.HighPrice - candle.LowPrice;
_prevClose = candle.ClosePrice;
_chopBars.Enqueue((tr, candle.HighPrice, candle.LowPrice));
while (_chopBars.Count > ChopLength)
_chopBars.Dequeue();
if (_chopBars.Count < ChopLength)
return null;
var range = _chopBars.Max(b => b.high) - _chopBars.Min(b => b.low);
var sumTr = _chopBars.Sum(b => b.tr);
if (range <= 0 || sumTr <= 0)
return null;
return 100m * (decimal)(Math.Log10((double)(sumTr / range)) / Math.Log10(ChopLength));
}
private bool InSession(DateTime time)
{
var parts = (SessionInput ?? string.Empty).Split('-');
if (parts.Length != 2
|| !int.TryParse(parts[0].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)
|| !int.TryParse(parts[1].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var end))
return true;
var startMinutes = start / 100 * 60 + start % 100;
var endMinutes = end / 100 * 60 + end % 100;
var minutes = (int)time.TimeOfDay.TotalMinutes;
if (startMinutes == endMinutes)
return true;
// A session whose start is after its end runs overnight.
return startMinutes < endMinutes
? minutes >= startMinutes && minutes < endMinutes
: minutes >= startMinutes || minutes < endMinutes;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, OnBalanceVolume, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class chop_flow_atr_scalp_strategy(Strategy):
"""
ChopFlow ATR Scalp strategy.
Inside the trading session, when the Choppiness Index is below ChopThreshold the market is trending: a flat position goes long
with OBV above its EMA and short with OBV below it. The exit is a symmetric stop and target placed AtrMultiplier ATRs from the entry.
"""
def __init__(self):
super(chop_flow_atr_scalp_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 1.5).SetGreaterThanZero().SetDisplay("ATR Multiplier", "ATR multiple for the stop and the target", "Risk")
self._chop_length = self.Param("ChopLength", 14).SetRange(2, 1000).SetDisplay("Chop Length", "Choppiness Index period", "Indicators")
self._chop_threshold = self.Param("ChopThreshold", 60.0).SetDisplay("Chop Threshold", "Choppiness level below which entries are allowed", "Indicators")
self._obv_ema_length = self.Param("ObvEmaLength", 10).SetGreaterThanZero().SetDisplay("OBV EMA Length", "EMA period applied to OBV", "Indicators")
self._session_input = self.Param("SessionInput", "1700-1600").SetDisplay("Session", "Trading session as HHMM-HHMM in UTC", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._obv_ema = None
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._chop_bars = []
self._prev_close = None
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(chop_flow_atr_scalp_strategy, self).OnReseted()
self._reset_state()
self._obv_ema = None
def OnStarted2(self, time):
super(chop_flow_atr_scalp_strategy, self).OnStarted2(time)
self._reset_state()
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
obv = OnBalanceVolume()
self._obv_ema = ExponentialMovingAverage()
self._obv_ema.Length = self._obv_ema_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, obv, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
volume_area = self.CreateChartArea()
if volume_area is not None:
self.DrawIndicator(volume_area, obv)
self.DrawIndicator(volume_area, self._obv_ema)
def _process_candle(self, candle, atr_value, obv_value):
if candle.State != CandleStates.Finished:
return
chop = self._update_choppiness(candle)
if not obv_value.IsFormed:
return
obv = obv_value.GetValue[Decimal](None)
obv_ema_value = process_value(self._obv_ema, obv, candle.OpenTime, True)
if self.Position > 0 and self._stop_price is not None and self._take_price is not None:
if candle.LowPrice <= self._stop_price or candle.HighPrice >= self._take_price:
self.SellMarket(self.Position)
self._stop_price = None
self._take_price = None
return
if self.Position < 0 and self._stop_price is not None and self._take_price is not None:
if candle.HighPrice >= self._stop_price or candle.LowPrice <= self._take_price:
self.BuyMarket(-self.Position)
self._stop_price = None
self._take_price = None
return
if self.Position != 0 or not atr_value.IsFormed or not obv_ema_value.IsFormed or chop is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if chop >= float(self._chop_threshold.Value) or not self._in_session(candle.OpenTime):
return
obv_ema = obv_ema_value.GetValue[Decimal](None)
distance = atr_value.GetValue[Decimal](None) * Decimal(self._atr_multiplier.Value)
close = candle.ClosePrice
if obv > obv_ema:
self.BuyMarket(self.Volume)
self._stop_price = close - distance
self._take_price = close + distance
elif obv < obv_ema:
self.SellMarket(self.Volume)
self._stop_price = close + distance
self._take_price = close - distance
def _update_choppiness(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._prev_close is not None:
tr = max(high, self._prev_close) - min(low, self._prev_close)
else:
tr = high - low
self._prev_close = float(candle.ClosePrice)
length = self._chop_length.Value
self._chop_bars.append((tr, high, low))
while len(self._chop_bars) > length:
self._chop_bars.pop(0)
if len(self._chop_bars) < length:
return None
price_range = max(b[1] for b in self._chop_bars) - min(b[2] for b in self._chop_bars)
sum_tr = sum(b[0] for b in self._chop_bars)
if price_range <= 0 or sum_tr <= 0:
return None
return 100.0 * math.log10(sum_tr / price_range) / math.log10(length)
def _in_session(self, time):
parts = (self._session_input.Value or "").split("-")
try:
start = int(parts[0].strip())
end = int(parts[1].strip())
except (ValueError, IndexError):
return True
if len(parts) != 2:
return True
start_minutes = start // 100 * 60 + start % 100
end_minutes = end // 100 * 60 + end % 100
minutes = time.Hour * 60 + time.Minute
if start_minutes == end_minutes:
return True
# A session whose start is after its end runs overnight.
if start_minutes < end_minutes:
return start_minutes <= minutes < end_minutes
return minutes >= start_minutes or minutes < end_minutes
def CreateClone(self):
return chop_flow_atr_scalp_strategy()