# chrono-ta
Timestamp-aware technical indicators for Rust.
[](https://github.com/austin-starks/chrono-ta/actions/workflows/ci.yml)
[](https://crates.io/crates/chrono-ta)
[](https://docs.rs/chrono-ta)
[](https://github.com/austin-starks/chrono-ta/blob/master/LICENSE)
[](https://www.rust-lang.org/)
`chrono-ta` computes moving averages, momentum, volatility, extrema, drawdown,
crossovers, true range, and VWAP over elapsed-time windows. Every streaming input carries a UTC
timestamp, so a 30-day indicator means 30 calendar days of observations rather
than the last 30 calls.

The animation uses the same irregular observations on both sides: upstream
`ta` retains the last N calls, while `chrono-ta` replaces a repeated time bucket
and expires observations according to elapsed time. Its reproducible Remotion
source lives in [`graphic/`](https://github.com/austin-starks/chrono-ta/tree/master/graphic).
The project began as a fork of [Greyblake's `ta`](https://github.com/greyblake/ta-rs),
but its input model and window semantics now differ substantially. It powers the
indicator path in [NexusTrade](https://nexustrade.io/).
## Why this exists
Observation-count windows are useful when every series has a fixed cadence. In
market systems, the same strategy may instead receive daily bars, hourly bars,
irregular historical data, or repeated live updates to the current bar.
`chrono-ta` makes time part of the indicator contract:
```text
(timestamp, value) -> indicator -> value for that point in time
```
That enables:
- windows expressed as `std::time::Duration`;
- expiration based on timestamps rather than call count;
- replacement of repeated updates within the current time bucket;
- scalar streaming and batched processing through the same stateful API;
- SIMD-backed batch paths for EMA and RSI, with scalar parity tests;
- bounded storage for long-running windowed indicators.
## `chrono-ta` versus `ta`
These crates share ancestry, not a drop-in-compatible API.
| Window definition | Elapsed time, such as 15 minutes or 30 days | Number of observations, such as 14 values |
| Streaming input | `(DateTime<Utc>, value)` | A value or market-data item |
| Repeated live updates | Replaces the current time bucket | Every call advances state |
| Batch API | `NextBatch` plus public SIMD primitives | Scalar `Next` |
| Indicator scope | Focused set used by the timestamped engine | Broader classic indicator catalog |
| Install name | `chrono-ta` | `ta` |
| Rust import | `chrono_ta` | `ta` |
Choose upstream `ta` when you want its larger indicator catalog and
observation-count semantics. Choose `chrono-ta` when timestamps, elapsed-time
expiration, repeated current-bar updates, or batch processing are part of the
problem.
## Install
Install the published crate:
```toml
[dependencies]
chrono-ta = "2.2"
```
Enable serialization when indicator state must survive a restart:
```toml
[dependencies]
chrono-ta = { version = "2.2", features = ["serde"] }
```
To test an unreleased GitHub revision instead:
```toml
[dependencies]
chrono-ta = { git = "https://github.com/austin-starks/chrono-ta" }
```
## Quick start
```rust
use chrono::{Duration as ChronoDuration, TimeZone, Utc};
use chrono_ta::indicators::ExponentialMovingAverage;
use chrono_ta::Next;
use std::time::Duration;
let mut ema = ExponentialMovingAverage::new(Duration::from_secs(3 * 60)).unwrap();
let start = Utc.with_ymd_and_hms(2026, 9, 20, 14, 30, 0).unwrap();
assert_eq!(ema.next((start, 2.0)), 2.0);
assert_eq!(
ema.next((start + ChronoDuration::minutes(1), 5.0)),
3.5
);
assert_eq!(
ema.next((start + ChronoDuration::minutes(2), 1.0)),
2.25
);
```
All indicators implement `Next<T>`. They also implement `Reset`, `Debug`,
`Display`, `Default`, and `Clone` where appropriate.
## Current-bar replacement
Streaming feeds often send several revisions of a bar before it closes. The
adaptive detector keeps those revisions from becoming several observations:
- windows shorter than five minutes use one-second buckets;
- intraday windows use one-minute buckets;
- windows of one day or longer use the library's daily-session gap rule.
Calling `next` twice inside the same bucket replaces the current observation
instead of advancing the indicator. Timestamps should therefore arrive in
nondecreasing order. This behavior is a core difference from upstream `ta`, not
an incidental optimization.
Indicators that operate on OHLCV bars accept an explicit `bucket_width`. This
makes the identity of a revisable live bar unambiguous instead of guessing its
cadence from the rolling window.
## Batch processing
`NextBatch` returns the same state transition as calling `next` repeatedly.
EMA and RSI use optimized batch implementations when no input would trigger
same-bucket replacement; other indicators use the trait's scalar fallback.
```rust
use chrono::{Duration as ChronoDuration, TimeZone, Utc};
use chrono_ta::indicators::RelativeStrengthIndex;
use chrono_ta::NextBatch;
use std::time::Duration;
let start = Utc.with_ymd_and_hms(2026, 9, 20, 0, 0, 0).unwrap();
let inputs = vec![
(start, 100.0),
(start + ChronoDuration::days(1), 102.0),
(start + ChronoDuration::days(2), 101.0),
];
let mut rsi = RelativeStrengthIndex::new(Duration::from_secs(14 * 86_400)).unwrap();
let values = rsi.next_batch(&inputs);
assert_eq!(values.len(), inputs.len());
```
The public `simd` module also exposes EMA, rate-of-change, reduction, rolling
mean, and rolling-standard-deviation primitives for callers that already own
contiguous slices.
## Indicators
| Trend and composition | Exponential Moving Average, Simple Moving Average, Rolling Sum, Lag / Value Ago, Cross Above, Cross Below |
| Momentum | Relative Strength Index, Rate of Change |
| Volatility | Bollinger Bands, Standard Deviation, Mean Absolute Deviation, True Range, Average True Range |
| Volume | Rolling VWAP, Anchored VWAP |
| Extrema and risk | Minimum, Maximum, Max Drawdown, Max Drawup |
The narrower catalog is intentional. Indicators present in upstream `ta`, such
as MACD, stochastic oscillators, and OBV, are not currently implemented
here. Do not select this crate on the assumption that every upstream indicator
is available.
`AverageTrueRange` is the arithmetic mean of true ranges inside an elapsed-time
window; it is not Wilder's observation-count recurrence. `RollingVwap` expires
contributions by elapsed time. `AnchoredVwap` accumulates until the caller invokes
`Reset::reset`. Both VWAP variants use typical price `(high + low + close) / 3`.
## OHLCV indicators
`DataItem` provides a validated OHLCV input, while the public `Open`, `High`,
`Low`, `Close`, and `Volume` traits let applications use their own bar types.
```rust
use chrono::{Duration as ChronoDuration, TimeZone, Utc};
use chrono_ta::indicators::{AverageTrueRange, RollingVwap};
use chrono_ta::{DataItem, Next};
use std::time::Duration;
let start = Utc.with_ymd_and_hms(2026, 9, 20, 14, 30, 0).unwrap();
let first = DataItem::builder()
.open(100.0)
.high(104.0)
.low(99.0)
.close(102.0)
.volume(1_000.0)
.build()
.unwrap();
let second = DataItem::builder()
.open(102.0)
.high(106.0)
.low(101.0)
.close(105.0)
.volume(1_500.0)
.build()
.unwrap();
let bucket = Duration::from_secs(60);
let mut atr = AverageTrueRange::new(Duration::from_secs(15 * 60), bucket).unwrap();
let mut vwap = RollingVwap::new(Duration::from_secs(15 * 60), bucket).unwrap();
assert_eq!(atr.next((start, first)), 5.0);
assert_eq!(atr.next((start + ChronoDuration::minutes(1), second)), 5.0);
assert!(vwap.next((start, first)).is_some());
```
## State and serialization
The optional `serde` feature serializes indicator state. Optimized derived
state is rebuilt when needed after deserialization, and the test suite covers
continuing an indicator after a round trip.
Serialized representations are an implementation detail, not a stable wire
format. Keep the crate version with persisted state and test migrations before
upgrading a long-lived store.
## Migrating from the old repository name
GitHub redirects the former `austin-starks/ta-rs-improved` URL, so dependencies
pinned to an existing commit continue to resolve. New dependencies should use
the `chrono-ta` package and URL.
To preserve existing `use ta::...` imports while moving to a new revision,
rename the dependency locally:
```toml
[dependencies]
ta = { package = "chrono-ta", git = "https://github.com/austin-starks/chrono-ta" }
```
The source imports can then remain unchanged even though the published package
is named `chrono-ta`.
## Development
```bash
cargo fmt --check
cargo test --all-targets --all-features
cargo test --doc --all-features
cargo doc --no-deps --all-features
cargo package --list
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for defect reports, test expectations,
and pull-request scope. Security problems should be reported privately through
[SECURITY.md](SECURITY.md).
## Releases
Published versions are available on [crates.io](https://crates.io/crates/chrono-ta),
with API documentation built by [docs.rs](https://docs.rs/chrono-ta). The
release checklist in [CONTRIBUTING.md](CONTRIBUTING.md) treats the registry
upload as a deliberate, irreversible step after the exact commit passes CI.
## NexusTrade
`chrono-ta` powers time-windowed technical indicators in
[NexusTrade](https://nexustrade.io/), an AI-assisted platform for researching,
testing, optimizing, and deploying systematic trading strategies.
The fork's original RSI correction is described in
[this development article](https://nexustrade.io/blog/i-used-an-ai-to-fix-a-major-bug-in-a-very-popular-open-source-technical-indicator-library-20231223).
## License and upstream credit
Released under the [MIT License](LICENSE). `chrono-ta` is derived from
[Greyblake's `ta`](https://github.com/greyblake/ta-rs), created by Sergey
Potapov and its contributors. Austin Starks maintains this timestamp-aware fork.