Indicators
Custom Indicators
Introduction
LEAN supports over 100 pre-built indicators, but a custom indicator is an indicator you define. It receives input, performs some calculation, and sets its output value. Custom indicators are helpful when you want to achieve any of the following results:
- Use an indicator that LEAN doesn't currently implement
- Combine built-in indicators beyond the logic of Indicator Extensions
- Create your own unique indicator for trading
Define Indicators
Custom indicators must implement the PythonIndicator
class. The indicator must have an Update
update
method and Name
name
, Time
time
, and
Value
value
attributes. The Update
update
method must accept an IndicatorDataPoint
, QuoteBar
, or TradeBar
and return a boolean that represents if the indicator is ready. The Time
time
attribute represents the last time you updated the indicator and the Value
value
attribute represents the current indicator value. The following definition provides an example of a custom simple moving average indicator.
Custom indicators subsclass the IndicatorBase<IndicatorDataPoint>
, BarIndicator
, or TradeBarIndicator
class, depending on the indicator type. The following definition provides an example of a custom simple moving average indicator that inherits the IndicatorBase<IndicatorDataPoint>
class. To view examples of indicators that inherit the BarIndicator
or TradeBarIndicator
class, see the AverageTrueRange or VolumeWeightedAveragePriceIndicator implementation in the LEAN GitHub repository.
public class CustomSimpleMovingAverage : IndicatorBase<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider { private RollingWindow<decimal> _window; public override bool IsReady => _window.IsReady; public int WarmUpPeriod => _window.Size; public CustomSimpleMovingAverage(string name, int period) : base(name) { _window = new RollingWindow<decimal>(period); } protected override decimal ComputeNextValue(IndicatorDataPoint input) { _window.Add(input.Value); return _window.Sum() / _window.Size; } public override void Reset() { _window.Reset(); base.Reset(); } }
class CustomSimpleMovingAverage(PythonIndicator): def __init__(self, name, period): self.name = name self.warm_up_period = period self.time = datetime.min self.value = 0 self.queue = deque(maxlen=period) def update(self, input: BaseData) -> bool: self.queue.appendleft(input.value) count = len(self.queue) self.time = input.time self.value = sum(self.queue) / count return count == self.queue.maxlen
The following definition provides an example of a custom money flow index indicator.
public class CustomMoneyFlowIndex : TradeBarIndicator, IIndicatorWarmUpPeriodProvider { private decimal _previousTypicalPrice; private RollingWindow<decimal> _negativeMoneyFlow; private RollingWindow<decimal> _positiveMoneyFlow; public override bool IsReady => _positiveMoneyFlow.IsReady; public int WarmUpPeriod => _positiveMoneyFlow.Size; public CustomMoneyFlowIndex(string name, int period) : base(name) { _negativeMoneyFlow = new(period); _positiveMoneyFlow = new(period); _previousTypicalPrice = 0m; } protected override decimal ComputeNextValue(TradeBar input) { var typicalPrice = (input.High + input.Low + input.Close) / 3; var moneyFlow = typicalPrice * input.Volume; _negativeMoneyFlow.Add(typicalPrice < _previousTypicalPrice ? moneyFlow: 0); _positiveMoneyFlow.Add(typicalPrice > _previousTypicalPrice ? moneyFlow: 0); _previousTypicalPrice = moneyFlow; var positiveMoneyFlowSum = _positiveMoneyFlow.Sum(); var totalMoneyFlow = positiveMoneyFlowSum + _negativeMoneyFlow.Sum(); return totalMoneyFlow == 0 ? 100m : 100m * positiveMoneyFlowSum / totalMoneyFlow; } public override void Reset() { _previousTypicalPrice = 0m; _negativeMoneyFlow.Reset(); _positiveMoneyFlow.Reset(); base.Reset(); } }
class CustomMoneyFlowIndex(PythonIndicator): def __init__(self, name, period): super().__init__() self.name = name self.value = 0 self.previous_typical_price = 0 self.negative_money_flow = deque(maxlen=period) self.positive_money_flow = deque(maxlen=period) def update(self, input): if not isinstance(input, TradeBar): raise TypeError('CustomMoneyFlowIndex.update: input must be a TradeBar') typical_price = (input.high + input.low + input.close) / 3 money_flow = typical_price * input.volume # We need to avoid double rounding errors if abs(self.previous_typical_price / typical_price - 1) < 1e-10: self.previous_typical_price = typical_price self.negative_money_flow.appendleft(money_flow if typical_price < self.previous_typical_price else 0) self.positive_money_flow.appendleft(money_flow if typical_price > self.previous_typical_price else 0) self.previous_typical_price = typical_price positive_money_flow_sum = sum(self.positive_money_flow) total_money_flow = positive_money_flow_sum + sum(self.negative_money_flow) self.value = 100 if total_money_flow != 0: self.value *= positive_money_flow_sum / total_money_flow return len(self.positive_money_flow) == self.positive_money_flow.maxlen
Create Indicators
You must define a custom indicator before you can create an instance of it.
To create a custom indicator, call the indicator constructor.
private CustomSimpleMovingAverage _sma; _sma = new CustomSimpleMovingAverage("My SMA", 10);
self.custom_sma = CustomSimpleMovingAverage("My SMA", 10)
Updates
The process to update custom indicators is the same process you use to update manual indicators. For more information about updating manual indicators, see Manual Updates or Automatic Updates.
Warm Up Indicators
The process to warm up custom indicators is the same process you use to warm up manual indicators.