Composable technical analysis and strategy engine for Rust.
Pure Rust technical indicators with a type-safe strategy composition API. No C dependencies. No FFI. No unsafe in the default build.
Every indicator is verified against TA-Lib reference outputs.
[dependencies]
mantis-ta = "0.5.3"Feed candles one at a time. Get values out. O(1) per update, zero heap allocations in the hot path.
use mantis_ta::prelude::*;
let mut ema = EMA::new(20);
let mut rsi = RSI::new(14);
for candle in candles.iter() {
if let Some(ema_val) = ema.next(candle) {
println!("EMA(20) = {:.2}", ema_val);
}
if let Some(rsi_val) = rsi.next(candle) {
println!("RSI(14) = {:.2}", rsi_val);
}
}Compute over a full series at once. Returns Vec<Option<f64>> aligned with input candles (None during warmup).
use mantis_ta::prelude::*;
let sma_values = SMA::new(50).calculate(&candles);
let bb_values = BollingerBands::new(20, 2.0).calculate(&candles);
for (i, bb) in bb_values.iter().enumerate() {
if let Some(bb) = bb {
println!("Bar {}: Upper={:.2} Mid={:.2} Lower={:.2}", i, bb.upper, bb.middle, bb.lower);
}
}Define complete trading strategies as composable, type-checked rules. Invalid strategies don't compile.
use mantis_ta::prelude::*;
use mantis_ta::strategy::*;
let strategy = Strategy::builder("Golden Cross Momentum")
.timeframe(Timeframe::D1)
.entry(
all_of([
ema(20).crosses_above(ema(50)),
rsi(14).is_between(40.0, 65.0),
volume().is_above(volume_sma(20).scaled(1.5)),
])
)
.exit(
any_of([
ema(20).crosses_below(ema(50)),
rsi(14).is_above(80.0),
])
)
.stop_loss(StopLoss::atr_multiple(14, 2.0))
.take_profit(TakeProfit::atr_multiple(14, 3.0))
.max_position_size_pct(5.0)
.build()?;
// Evaluate against historical data
let signals: Vec<Signal> = strategy.evaluate(&candles)?;
// Or stream live — same strategy, bar by bar
let mut engine = strategy.into_engine();
for candle in live_feed {
match engine.next(&candle) {
Signal::Entry(Side::Long) => { /* open long */ },
Signal::Exit(reason) => { /* close position */ },
Signal::Hold => { /* wait */ },
_ => {}
}
}Honest simulation with realistic slippage, commissions, and next-bar execution.
use mantis_ta::backtest::*;
let result = backtest(&strategy, &candles, &BacktestConfig::default())?;
println!("Return: {:.2}%", result.metrics.total_return_pct);
println!("Sharpe Ratio: {:.2}", result.metrics.sharpe_ratio);
println!("Max Drawdown: {:.2}%", result.metrics.max_drawdown_pct);
println!("Win Rate: {:.2}%", result.metrics.win_rate_pct);
println!("Trades: {}", result.metrics.total_trades);v0.5.0 Batch A: SMA · EMA · WMA · DEMA · TEMA · MACD · ADX
Future: Ichimoku · Parabolic SAR · Supertrend
v0.5.0 Batch A: RSI · Stochastic · CCI · Williams %R · ROC
Future: MFI
v0.5.0 Batch A: Bollinger Bands · ATR · Standard Deviation
Future: Keltner Channels
OBV · Volume SMA · VWAP · Accumulation/Distribution
Pivot Points · Donchian Channels · Fibonacci Retracement
See the full indicator list in the API docs.
[dependencies]
mantis-ta = { version = "0.5", features = ["strategy", "backtest"] }| Feature | Default | Description |
|---|---|---|
serde |
✓ | Serialize strategies, indicators, and results to JSON |
strategy |
✓ | Strategy composition engine (v0.2.0+) |
backtest |
✓ | Backtesting engine with metrics (v0.4.0+) |
ndarray |
Interop with the ndarray ecosystem |
|
full-indicators |
All 50+ indicators (default includes 30 most common) | |
simd |
SIMD-accelerated batch computation (uses unsafe) |
|
all |
Everything |
- Correctness first. Every indicator verified against TA-Lib (< 1e-10 relative error).
- Streaming-first. O(1) incremental updates for live data. Batch is also first-class.
- Zero allocation in the hot path.
next()never heap-allocates. - No unsafe by default. Safe Rust is fast enough.
- Type system enforces validity. A strategy without a stop-loss is a compile error, not a runtime surprise.
- Honest backtesting. No lookahead bias. Slippage and commissions are mandatory, not optional.
Benchmarked on Apple M-series, single core:
| Operation | Time |
|---|---|
| EMA(20) per bar (streaming) | < 100 ns |
| RSI(14) batch, 2000 bars | < 15 µs |
| Strategy eval (5 conditions), 2000 bars | < 200 µs |
| Full backtest, 2 years daily | < 5 ms |
Run benchmarks yourself: cargo bench
Implement the Indicator trait to create your own:
use mantis_ta::prelude::*;
pub struct MyIndicator {
period: usize,
buffer: Vec<f64>,
}
impl Indicator for MyIndicator {
type Output = f64;
fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
self.buffer.push(candle.close);
if self.buffer.len() < self.period {
return None;
}
// Your calculation here
Some(self.buffer.iter().sum::<f64>() / self.period as f64)
}
fn warmup_period(&self) -> usize { self.period }
fn reset(&mut self) { self.buffer.clear(); }
fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
Box::new(self.clone())
}
}Contributions welcome! Please read CONTRIBUTING.md before opening a PR.
Adding a new indicator? See the Contributor Guide for the full checklist: implement the trait, add TA-Lib verification, write benchmarks, document it.
Licensed under either of:
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate shall be dual-licensed as above, without any additional terms or conditions.