Guide: Creating an Indicator
Advanced
You do not need this page for basic usage. Start with Quick Start if you are new to Hexital.
All built-in indicators are dataclasses that subclass Indicator and implement _calculate_reading(). Readings are stored on each Candle and updated incrementally on append().
Which recipe do I need?
Need hidden state between candles? → Recipe B (`add_state()`)
Need other indicators as inputs? → Recipe C (child indicators)
Just math on candle fields? → Recipe A (single method)
One-off pattern without a full class? → Amorph
| Pattern | Built-in example | Child helpers |
|---|---|---|
| Simple | SMA | — |
| Stateful | RSI | add_child_managed() |
| Composite | BBANDS | add_child() |
Generated names
Hexital builds indicator.name automatically in _generate_name() from _name_parts(). You normally do not override _generate_name().
| Behaviour | Detail |
|---|---|
| Default parts | period and source, when those fields exist on the indicator |
Default source |
Omitted from the name (e.g. "close" → SMA_10, not SMA_10_close) |
| Extra parameters | Override _name_parts() to list the dataclass fields to include |
| Nested outputs | Read with : — e.g. strategy.reading("MACD_12_26_9:signal") |
: in names |
Replaced with - in generated indicator names (nested paths still use : at lookup time) |
Override _format_name_part() on Indicator only when a field needs special formatting (e.g. timedelta anchors on VWAP).
Recipe A — Simple indicator
Use when the reading depends only on candle fields and prior readings of this indicator — no hidden state, no sub-indicators.
Based on SMA:
from dataclasses import dataclass, field
from hexital import Indicator
@dataclass(kw_only=True)
class MyAverage(Indicator[float | None]):
_name: str = field(init=False, default="MyAverage")
period: int = 10
source: str = "close"
def _calculate_reading(self, index: int) -> float | None:
if self.prev_exists():
prev = self.prev_reading()
old = self.reading(self.source, index - self.period)
new = self.reading(self.source)
if prev is not None and old is not None and new is not None:
return prev - (old - new) / self.period
return None
if self.reading_period(self.period, self.source):
return self.candles_average(self.period, self.source)
return None
Useful inherited helpers inside _calculate_reading():
self.reading(source)— value at the current indexself.prev_reading(source)— value at the previous indexself.reading_period(n, source)— enough history available?self.candles_average(n, source)— bootstrap the first reading
Recipe B — Stateful indicator
Use when you need to remember values between candles that are not part of the public output — smoothed gains/losses, intermediate EMA inputs, etc.
Based on RSI:
from dataclasses import dataclass, field
from hexital import Indicator, State
@dataclass(kw_only=True)
class MyOscillator(Indicator[float | None]):
_name: str = field(init=False, default="MyOscillator")
period: int = 14
source: str = "close"
def _initialise(self):
self._state = self.add_state()
def _calculate_reading(self, index: int) -> float | None:
gains = None
losses = None
if self.prev_exists():
change = self.prev_src() - self.src()
change_gain = -change if change < 0 else 0.0
change_loss = change if change > 0 else 0.0
gains = (
(self._state.prev("gain") * (self.period - 1)) + change_gain
) / self.period
losses = (
(self._state.prev("loss") * (self.period - 1)) + change_loss
) / self.period
self._state.update(gain=gains, loss=losses)
if gains is not None and losses is not None:
return 100.0 - (100.0 / (1.0 + (gains / losses)))
return None
add_state()holds hidden state viaset()/update()/prev()src()/prev_src()— shorthand for reading your configuredsourcefieldclose,open, etc. — OHLCV properties;prev("close"),at(-2, "high")for indexed readsadd_child_managed()— child runs only when you invoke it (viaset_reading()orcalculate_index())
Recipe C — Composite indicator
Use when your indicator depends on other indicators (moving averages, standard deviation, ATR, etc.).
Based on BBANDS:
from dataclasses import dataclass, field
from hexital import Indicator
from hexital import SMA, STDEV
@dataclass(kw_only=True)
class MyBands(Indicator[dict[str, float | None]]):
_name: str = field(init=False, default="MyBands")
period: int = 5
source: str = "close"
_std: float = field(init=False, default=2.0)
def _initialise(self):
self.sub_stdev = self.add_child(STDEV(source=self.source, period=self.period))
self.sub_sma = self.add_child(SMA(source=self.source, period=self.period))
def _calculate_reading(self, index: int) -> dict[str, float | None]:
if not (self.sub_sma.exists() and self.sub_stdev.exists()):
return {"lower": None, "mid": None, "upper": None}
sma = self.sub_sma.reading()
stdev = self.sub_stdev.reading()
return {
"mid": sma,
"lower": sma - (stdev * self._std),
"upper": sma + (stdev * self._std),
}
Child timing
| Method | When it runs |
|---|---|
add_child(...) |
Before parent's _calculate_reading() (default) |
add_child_after(...) |
After the parent's reading is stored |
add_child_managed(...) |
Only when you call it explicitly |
For full control, pass when= using ChildWhen:
Amorph — functions as indicators
For one-off signals or pattern checks without writing a full class, use Amorph:
from hexital import Amorph
from hexital.analysis.patterns import doji
pattern = Amorph(analysis=doji)
pattern.append(candles)
pattern.calculate()
Using a custom indicator in Hexital
from hexital import Candle, Hexital
from my_indicators import MyBands
strategy = Hexital("demo", candles, [MyBands(period=20)])
strategy.append(new_candle)
print(strategy.reading("MyBands_20"))
See also the Features — Custom Indicators example.