Metadata-Version: 2.5
Name: candlemath
Version: 0.1.0
Summary: Technical indicators in plain Python — no dependencies, verified, causal
License: MIT License
        
        Copyright (c) 2026 candlemath contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: adx,atr,backtesting,bollinger,ema,indicators,macd,no-dependencies,rsi,technical-analysis,trading
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: test
Requires-Dist: pandas>=1.3; extra == 'test'
Description-Content-Type: text/markdown

# candlemath

Technical indicators in plain Python. No dependencies, verified against an
independent implementation, and provably free of lookahead.

```python
import candlemath as cm

closes = [44.34, 44.09, 44.15, 43.61, 44.33, ...]

rsi = cm.rsi(closes, 14)          # same length as closes, None where undefined
fast = cm.ema(closes, 12)
atr = cm.atr(highs, lows, closes, 14)
```

## Why this exists

There is no shortage of TA libraries. This one exists for three specific
reasons.

**No dependencies.** The whole thing is one module of standard-library Python.
`ta` and `pandas-ta` pull in pandas; TA-Lib needs a C library compiled and
installed first. If you are working inside a small container, a locked-down
VPS, an embedded runtime, or anywhere `pip install pandas` is a conversation
rather than a command, that is a real cost. On a few hundred bars the scalar
loops here run in microseconds, which is well below the point where any of it
matters.

**Verified, not asserted.** Every indicator is cross-checked against a separate
implementation written with pandas — different code, different author, no
shared logic. Agreement between two independent implementations is evidence;
a test that re-derives a value the same way the library does proves nothing.
You do not have to take the table below on faith, either: `validate.py`
regenerates it.

**Causal by construction, and tested for it.** The value at bar `i` depends only
on bars up to `i`. Appending new bars never changes an already-computed value.
This is the property that decides whether a backtest means anything, and it is
checked directly rather than assumed — see [Lookahead](#lookahead) below.

## Install

```bash
pip install candlemath
```

Or just copy `candlemath/indicators.py` into your project. It is a single file
with no imports beyond `__future__`.

Python 3.9+.

## Accuracy

Output of ``python validate.py`` on a 400-bar series. The exact digits shift a
little between platforms and Python versions - what the table asserts is the
*magnitude*: the cross-check column sits at floating-point noise, and the
causality column is exactly zero everywhere.

| indicator  | vs pandas | causality |
|------------|-----------|-----------|
| sma(50)    | 3.13e-12  | 0.00e+00  |
| ema(50)    | 0.00e+00  | 0.00e+00  |
| rma(14)    | 2.84e-14  | 0.00e+00  |
| stdev(20)  | 8.92e-12  | 0.00e+00  |
| rsi(14)    | 0.00e+00  | 0.00e+00  |
| true_range | 0.00e+00  | 0.00e+00  |
| atr(14)    | 0.00e+00  | 0.00e+00  |
| macd hist  | n/a       | 0.00e+00  |
| bollinger  | n/a       | 0.00e+00  |
| adx(14)    | n/a       | 0.00e+00  |

**vs pandas** is the worst absolute difference against the independent
implementation. The non-zero figures are floating-point summation noise, not
disagreement — `sma` accumulates a running sum where pandas re-sums the window.

**causality** is the worst change to an already-computed value when more bars
arrive. Zero is the only acceptable number here.

### On Wilder's published table

RSI is usually checked against the worked example in Wilder's *New Concepts in
Technical Trading Systems*. Our first value is 70.4641 where the table says
70.53 — a gap of 0.066. The last value is 37.7888 against 37.77, a gap of 0.019.

The gap shrinks along the series, and that is the tell. Wilder computed the
table by hand, rounding to two decimals at every step of the recurrence; the
rounding error is largest at the seed and decays as the recursion washes it out.
Ours are the unrounded values, and they match the pandas implementation exactly.
The test suite asserts the decay rather than just the tolerance, so an
implementation that drifts the other way fails.

## Lookahead

An indicator looks ahead when its value at some bar depends on bars that had not
printed yet. It is easy to do by accident — centring a window, normalising by a
series-wide maximum, or reading `close` on a candle that has not closed. The
result is a strategy that is profitable in a backtest and worthless live.

The test suite checks the absence of it directly. Each indicator is computed
over a full series and over a truncated prefix, and every overlapping value must
agree to 1e-10. There is also a stricter replay test: feed the series one bar at
a time, as a live system would, and the last value of each incremental run must
equal the batch value at that bar.

```python
full = cm.rsi(closes, 14)
part = cm.rsi(closes[:25], 14)
assert full[:25] == part          # holds, to floating-point precision
```

One place where a lag is unavoidable: `swing_points` confirms a pivot only after
`right` further bars, so the last `right` bars never yield one. That is stated
rather than hidden, and tested.

## API

Every function returns a list the same length as its input, holding `None` where
the indicator is not yet defined. That is what makes it safe to zip series
together — nothing is silently shifted against anything else.

| function | returns |
|---|---|
| `sma(values, period)` | simple moving average |
| `ema(values, period)` | exponential MA, seeded with the SMA of the first `period` values |
| `rma(values, period)` | Wilder's smoothing (RMA / SMMA) |
| `stdev(values, period)` | rolling population standard deviation |
| `rsi(closes, period=14)` | Relative Strength Index |
| `true_range(highs, lows, closes)` | true range (no undefined positions) |
| `atr(highs, lows, closes, period=14)` | Average True Range |
| `macd(closes, fast=12, slow=26, signal=9)` | `(line, signal, histogram)` |
| `bollinger(closes, period=20, mult=2.0)` | `(upper, middle, lower)` |
| `adx(highs, lows, closes, period=14)` | `(adx, di_plus, di_minus)` |
| `swing_points(highs, lows, left=2, right=2)` | `(highs, lows)` as `(index, price)` |
| `cluster_levels(points, tolerance_pct=0.6)` | merged price levels with touch counts |

### RMA is not EMA

Worth stating on its own, because it is the most common bug in hand-rolled
RSI, ATR and ADX implementations:

```
Wilder (rma):  prev * (period - 1) / period  +  value / period
EMA:           prev * (1 - k)                +  value * k,   k = 2 / (period + 1)
```

For period 14 that is a weight of 0.0714 against 0.1333 — nearly double. Swap
one for the other and the output still looks like a plausible RSI, which is
exactly why the mistake survives review. There is a test whose only job is to
fail if someone "simplifies" `rma` into a call to `ema`.

## Tests

```bash
python -m unittest discover -s tests -t .
```

48 tests across three files:

- `test_reference.py` — published reference values, length invariants,
  definitional properties (RSI bounded 0–100 and saturating, true range widening
  on gaps, DI flipping with trend direction, bands collapsing at zero volatility)
- `test_crosscheck.py` — agreement with the pandas implementation
- `test_causality.py` — the no-lookahead guarantee

pandas is a test-only dependency. Without it the cross-check tests skip and
everything else still runs.

## What this is not

Not vectorised. On hundreds of thousands of bars a pandas or NumPy
implementation will be far faster, and you should use one. This is built for
correctness and portability on the scale where those matter more.

Not comprehensive. Twelve functions, chosen because they compose into most of
what people actually use. Ichimoku, Keltner, VWAP and the rest are absent on
purpose — a small surface that is fully verified beats a large one that is not.

Not a trading system. It computes numbers. What you do with them is your
problem, and most of what people do with them loses money.

## License

MIT.
