Source code for backtrader.indicators.contrib.karacatica_indicator

#!/usr/bin/env python
"""Functional-test indicators migrated to contrib.

Generated from a single functional strategy module to preserve file-local
helper functions and constants without cross-test name collisions.
"""

from .. import Indicator

__all__ = [
    "KaracaticaIndicator",
]


[docs] class KaracaticaIndicator(Indicator): """Reconstructs Karacatica from MQ5 source. Uses ATR(iPeriod), ADX(iPeriod) +DI/-DI, and close-vs-close(iPeriod-ago) to generate buy/sell arrows with direction latch to avoid repeats. """ lines = ("buy_arrow", "sell_arrow") params = (("iperiod", 70),) def __init__(self): """Initialize the ATR scaling factor, direction latch, and min period.""" self._s = 1.5 / 2.0 self._ltr = 0 # 0=none, 1=last was buy, 2=last was sell self.addminperiod(int(self.p.iperiod) + 2) def _calc_atr(self): period = int(self.p.iperiod) total = 0.0 for i in range(period): hi = float(self.data.high[-i]) lo = float(self.data.low[-i]) prev_c = float(self.data.close[-(i + 1)]) total += max(hi - lo, abs(hi - prev_c), abs(prev_c - lo)) return total / period def _calc_adx_di(self): period = int(self.p.iperiod) plus_dm_sum = 0.0 minus_dm_sum = 0.0 tr_sum = 0.0 for i in range(period): hi = float(self.data.high[-i]) lo = float(self.data.low[-i]) prev_hi = float(self.data.high[-(i + 1)]) prev_lo = float(self.data.low[-(i + 1)]) prev_c = float(self.data.close[-(i + 1)]) up_move = hi - prev_hi down_move = prev_lo - lo plus_dm = up_move if (up_move > down_move and up_move > 0) else 0.0 minus_dm = down_move if (down_move > up_move and down_move > 0) else 0.0 tr = max(hi - lo, abs(hi - prev_c), abs(prev_c - lo)) plus_dm_sum += plus_dm minus_dm_sum += minus_dm tr_sum += tr if tr_sum == 0: return 0.0, 0.0 plus_di = 100.0 * plus_dm_sum / tr_sum minus_di = 100.0 * minus_dm_sum / tr_sum return plus_di, minus_di
[docs] def next(self): """Emit ATR-offset buy/sell arrows on latched directional breakouts. Computes ATR and +DI/-DI over ``iperiod`` and, when the close exceeds its value ``iperiod`` bars ago with +DI dominant (and the last arrow was not a buy), places a buy arrow below the low; the symmetric condition places a sell arrow above the high. The direction latch prevents consecutive arrows of the same side. """ period = int(self.p.iperiod) if len(self.data) < period + 2: self.lines.buy_arrow[0] = 0.0 self.lines.sell_arrow[0] = 0.0 return atr = self._calc_atr() plus_di, minus_di = self._calc_adx_di() cur_close = float(self.data.close[0]) past_close = float(self.data.close[-period]) cur_high = float(self.data.high[0]) cur_low = float(self.data.low[0]) buy_val = 0.0 sell_val = 0.0 if cur_close > past_close and plus_di > minus_di and self._ltr != 1: buy_val = cur_low - atr * self._s self._ltr = 1 if cur_close < past_close and plus_di < minus_di and self._ltr != 2: sell_val = cur_high + atr * self._s self._ltr = 2 self.lines.buy_arrow[0] = buy_val self.lines.sell_arrow[0] = sell_val