Kwant is a candle-driven technical indicators library for Rust, built around the workflow used in a Hyperliquid trading terminal. It is designed for two update modes:
update_before_close(price)for provisional, in-candle values on every tickupdate_after_close(price)for the final committed candle close
The crate exposes a shared Indicator trait, a shared Price input type, and a Value enum that carries each indicator's output shape.
kwant = { git = "https://github.com/0xNoSystem/Kwant" }Every indicator implements the same trait:
pub trait Indicator: Debug + Sync + Send {
fn update_after_close(&mut self, last_price: Price);
fn update_before_close(&mut self, last_price: Price);
fn load(&mut self, price_data: &[Price]);
fn is_ready(&self) -> bool;
fn get_last(&self) -> Option<Value>;
fn reset(&mut self);
fn period(&self) -> u32;
}All indicators consume the same Price struct:
pub struct Price {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub open_time: u64,
pub close_time: u64,
pub vlm: f64,
}In practice, most indicators only use a subset of these fields:
- close-based indicators use
close - range-based indicators use
high,low, and often previousclose - volume-based indicators use
vlm
- Input:
high,low,close - Output:
Value::CciValue(f64) - Formula:
TP = (high + low + close) / 3SMA_TP = mean(TP, period)MD = mean(|TP_i - SMA_TP|, period)CCI = (TP - SMA_TP) / (0.015 * MD)
- Input:
close - Output:
Value::MacdValue { macd, signal, histogram } - Formula:
EMA_fast = EMA(close, fast)EMA_slow = EMA(close, slow)macd = EMA_fast - EMA_slowsignal = EMA(macd, signal_period)histogram = macd - signal
- Input:
close - Output:
Value::RocValue(f64) - Formula:
ROC = ((close_t / close_{t-period}) - 1) * 100
- Input:
close - Output:
Value::RsiValue(f64) - Formula:
change = close_t - close_{t-1}gain = max(change, 0)loss = max(-change, 0)- Wilder smoothing:
avg_gain = ((prev_avg_gain * (period - 1)) + gain) / periodavg_loss = ((prev_avg_loss * (period - 1)) + loss) / period
RSI = 100 - (100 / (1 + avg_gain / avg_loss))- if
avg_loss == 0, RSI is100
- Input:
close - Output:
Value::SmaRsiValue(f64) - Formula:
- first compute RSI
- then compute a simple moving average over the last
smoothing_lengthRSI values
- Input:
close - Output:
Value::StochRsiValue { k, d } - Formula:
- first compute RSI
raw_k = (RSI - min(RSI, period)) / (max(RSI, period) - min(RSI, period))%K = SMA(raw_k, k_smoothing) * 100%D = SMA(%K, d_smoothing) * 100
- Input:
high,low,close - Output:
Value::AdxValue(f64) - Formula:
TR = max(high - low, |high - prev_close|, |low - prev_close|)+DM = high - prev_highwhen it exceeds down move and is positive, otherwise0-DM = prev_low - lowwhen it exceeds up move and is positive, otherwise0- smooth
TR,+DM, and-DMwith Wilder smoothing overdi_length +DI = 100 * (+DM_smooth / TR_smooth)-DI = 100 * (-DM_smooth / TR_smooth)DX = 100 * |+DI - -DI| / (+DI + -DI)ADXis the Wilder average ofDXoverperiod
- Input:
close - Output:
Value::DemaValue(f64) - Formula:
EMA1 = EMA(close, period)EMA2 = EMA(EMA1, period)DEMA = 2 * EMA1 - EMA2
- Input:
close - Output:
Value::EmaValue(f64) - Formula:
- seed with
SMA(close, period) alpha = 2 / (period + 1)EMA_t = alpha * close_t + (1 - alpha) * EMA_{t-1}
- seed with
Ema also exposes get_slope(), which reports the percentage change from the last confirmed EMA to the current EMA value.
- Input:
close - Output:
Value::EmaCrossValue { short, long, trend } - Formula:
- compute a short EMA and a long EMA
trend = short >= long
- Input:
high,low,close - Output:
Value::IchimokuValue { tenkan, kijun, span_a, span_b, chikou } - Formula:
tenkan = (highest_high(tenkan) + lowest_low(tenkan)) / 2kijun = (highest_high(kijun) + lowest_low(kijun)) / 2span_a = (tenkan + kijun) / 2span_b = (highest_high(senkou_b) + lowest_low(senkou_b)) / 2chikou = current close
The crate returns the raw line values. Plotting offsets for senkou spans and chikou are left to the consumer.
- Input:
close - Output:
Value::SmaValue(f64) - Formula:
SMA = mean(close, period)
- Input:
close - Output:
Value::TemaValue(f64) - Formula:
EMA1 = EMA(close, period)EMA2 = EMA(EMA1, period)EMA3 = EMA(EMA2, period)TEMA = 3 * EMA1 - 3 * EMA2 + EMA3
- Input:
high,low,close - Output:
Value::AtrValue(f64) - Formula:
TR = max(high - low, |high - prev_close|, |low - prev_close|)- initial ATR is the mean of the warmup true ranges
- afterward:
ATR_t = ((ATR_{t-1} * (period - 1)) + TR_t) / period
Atr also exposes normalized(price), which returns ATR as a percentage of a supplied reference price.
- Input:
close - Output:
Value::BollingerValue { upper, mid, lower, width } - Formula:
mid = SMA(close, period)stddev = sqrt(E[x^2] - E[x]^2)using population variance over the rolling windowupper = mid + std_multiplier * stddevlower = mid - std_multiplier * stddevwidth = ((upper - lower) / |mid|) * 100
- Input:
close - Output:
Value::HistVolatilityValue(f64) - Formula:
r_t = ln(close_t / close_{t-1})- compute rolling sample standard deviation of
r_t - annualize with:
HV = stddev(log_returns, period) * sqrt(365) * 100
- Input:
close,vlm - Output:
Value::ObvValue(f64) - Formula:
- if
close_t > close_{t-1}:OBV_t = OBV_{t-1} + volume_t - if
close_t < close_{t-1}:OBV_t = OBV_{t-1} - volume_t - otherwise:
OBV_t = OBV_{t-1} - the implementation initializes from
0
- if
- Input:
vlm - Output:
Value::VolumeMaValue(f64) - Formula:
VolumeMA = mean(volume, period)
- Input:
close,vlm - Output:
Value::VwapDeviationValue(f64) - Formula:
VWAP = sum(price * volume) / sum(volume)over the rolling windowvariance = sum(price^2 * volume) / sum(volume) - VWAP^2stddev = sqrt(variance)deviation = (close - VWAP) / stddev
update_before_closeis for live, in-candle recalculation and may be called many times for the same candleupdate_after_closecommits the candle and advances the rolling stateload(&[Price])is equivalent to replaying a historical series throughupdate_after_close
use kwant::indicators::{Ema, Indicator, Price, Value};
fn main() {
let mut ema = Ema::new(3);
let candles = vec![
Price {
open: 100.0,
high: 102.0,
low: 99.0,
close: 101.0,
open_time: 0,
close_time: 0,
vlm: 10.0,
},
Price {
open: 101.0,
high: 103.0,
low: 100.0,
close: 102.0,
open_time: 1,
close_time: 1,
vlm: 11.0,
},
Price {
open: 102.0,
high: 104.0,
low: 101.0,
close: 103.0,
open_time: 2,
close_time: 2,
vlm: 12.0,
},
];
ema.load(&candles);
let live_price = Price {
open: 103.0,
high: 105.0,
low: 102.0,
close: 104.0,
open_time: 3,
close_time: 3,
vlm: 13.0,
};
ema.update_before_close(live_price);
if let Some(Value::EmaValue(value)) = ema.get_last() {
println!("Provisional EMA: {value:.2}");
}
ema.update_after_close(live_price);
if let Some(Value::EmaValue(value)) = ema.get_last() {
println!("Confirmed EMA: {value:.2}");
}
}PRs are welcome. If you use Kwant in a live trading system and want to add indicators or tighten parity with an external charting platform, open an issue or send a patch.
MIT