feat: derivatives basis & calendar-spread indicators (part 3 of 3) (#128)
* feat(derivatives): TermStructureBasis indicator (core) * feat(derivatives): CalendarSpread indicator (core) * feat(derivatives): Python, Node and WASM bindings for basis & calendar-spread indicators * test(derivatives): Python and Node tests for basis & calendar-spread indicators * docs(derivatives): README row + counter 242->244, CHANGELOG part 3; fuzz basis indicators
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
//! Calendar Spread — the dated future's relative premium to the perpetual.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Calendar Spread — the relative spread between a dated (e.g. quarterly)
|
||||
/// futures price and the perpetual mark price.
|
||||
///
|
||||
/// ```text
|
||||
/// spread = (futuresPrice − markPrice) / markPrice
|
||||
/// ```
|
||||
///
|
||||
/// A calendar (or inter-delivery) spread trades the *near* leg against the
|
||||
/// *far* leg — here the perpetual against a dated future. The relative spread is
|
||||
/// the roll yield available between the two contracts: positive when the future
|
||||
/// trades over the perpetual (contango roll), negative when under
|
||||
/// (backwardation). Where [`TermStructureBasis`] measures the future against
|
||||
/// spot, this measures it against the perpetual — the leg a perp-vs-future
|
||||
/// basis trade actually holds. The output is a fraction; multiply by `10_000`
|
||||
/// for basis points.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// [`TermStructureBasis`]: crate::TermStructureBasis
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{CalendarSpread, DerivativesTick, Indicator};
|
||||
///
|
||||
/// fn tick(futures: f64, mark: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut cs = CalendarSpread::new();
|
||||
/// // futures 101 vs perpetual mark 100 -> 0.01.
|
||||
/// assert!((cs.update(tick(101.0, 100.0)).unwrap() - 0.01).abs() < 1e-12);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarSpread {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl CalendarSpread {
|
||||
/// Construct a new calendar-spread indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for CalendarSpread {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
Some((tick.futures_price - tick.mark_price) / tick.mark_price)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CalendarSpread"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(futures: f64, mark: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let cs = CalendarSpread::new();
|
||||
assert_eq!(cs.name(), "CalendarSpread");
|
||||
assert_eq!(cs.warmup_period(), 1);
|
||||
assert!(!cs.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_over_perp_is_positive() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
let out = cs.update(tick(101.0, 100.0)).unwrap();
|
||||
assert!((out - 0.01).abs() < 1e-12);
|
||||
assert!(cs.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_under_perp_is_negative() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
let out = cs.update(tick(99.0, 100.0)).unwrap();
|
||||
assert!((out + 0.01).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_is_zero() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
assert_eq!(cs.update(tick(100.0, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(100.0 + f64::from(i % 5), 100.0))
|
||||
.collect();
|
||||
let mut a = CalendarSpread::new();
|
||||
let mut b = CalendarSpread::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut cs = CalendarSpread::new();
|
||||
cs.update(tick(101.0, 100.0));
|
||||
assert!(cs.is_ready());
|
||||
cs.reset();
|
||||
assert!(!cs.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod balance_of_power;
|
||||
mod beta;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
mod calendar_spread;
|
||||
mod calmar_ratio;
|
||||
mod camarilla_pivots;
|
||||
mod cci;
|
||||
@@ -204,6 +205,7 @@ mod td_risk_level;
|
||||
mod td_sequential;
|
||||
mod td_setup;
|
||||
mod tema;
|
||||
mod term_structure_basis;
|
||||
mod three_inside;
|
||||
mod three_outside;
|
||||
mod three_soldiers_or_crows;
|
||||
@@ -271,6 +273,7 @@ pub use balance_of_power::BalanceOfPower;
|
||||
pub use beta::Beta;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use calendar_spread::CalendarSpread;
|
||||
pub use calmar_ratio::CalmarRatio;
|
||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||
pub use cci::Cci;
|
||||
@@ -446,6 +449,7 @@ pub use td_risk_level::{TdRiskLevel, TdRiskLevelOutput};
|
||||
pub use td_sequential::{TdSequential, TdSequentialOutput};
|
||||
pub use td_setup::TdSetup;
|
||||
pub use tema::Tema;
|
||||
pub use term_structure_basis::TermStructureBasis;
|
||||
pub use three_inside::ThreeInside;
|
||||
pub use three_outside::ThreeOutside;
|
||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||
@@ -784,6 +788,8 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"LongShortRatio",
|
||||
"TakerBuySellRatio",
|
||||
"LiquidationFeatures",
|
||||
"TermStructureBasis",
|
||||
"CalendarSpread",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -840,6 +846,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 237, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 239, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Term-Structure Basis — the dated future's relative premium to spot.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Term-Structure Basis — the relative basis between a dated (e.g. quarterly)
|
||||
/// futures price and the spot index.
|
||||
///
|
||||
/// ```text
|
||||
/// basis = (futuresPrice − indexPrice) / indexPrice
|
||||
/// ```
|
||||
///
|
||||
/// Where [`FundingBasis`] measures the *perpetual*'s premium to spot, this
|
||||
/// measures a *dated future*'s — the term-structure carry that a calendar or
|
||||
/// cash-and-carry trade harvests as the contract converges to spot at expiry. A
|
||||
/// positive basis is contango (futures above spot), a negative one backwardation.
|
||||
/// The output is a fraction (e.g. `0.02` = 2%); multiply by `10_000` for basis
|
||||
/// points.
|
||||
///
|
||||
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
|
||||
/// tick.
|
||||
///
|
||||
/// [`FundingBasis`]: crate::FundingBasis
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, TermStructureBasis};
|
||||
///
|
||||
/// fn tick(futures: f64, index: f64) -> DerivativesTick {
|
||||
/// DerivativesTick::new(0.0, index, index, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
/// .unwrap()
|
||||
/// }
|
||||
///
|
||||
/// let mut ts = TermStructureBasis::new();
|
||||
/// // futures 102 vs index 100 -> 0.02 (2% contango).
|
||||
/// assert!((ts.update(tick(102.0, 100.0)).unwrap() - 0.02).abs() < 1e-12);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TermStructureBasis {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl TermStructureBasis {
|
||||
/// Construct a new term-structure basis indicator.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TermStructureBasis {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
Some((tick.futures_price - tick.index_price) / tick.index_price)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TermStructureBasis"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn tick(futures: f64, index: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, index, index, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let ts = TermStructureBasis::new();
|
||||
assert_eq!(ts.name(), "TermStructureBasis");
|
||||
assert_eq!(ts.warmup_period(), 1);
|
||||
assert!(!ts.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contango_is_positive() {
|
||||
let mut ts = TermStructureBasis::new();
|
||||
let out = ts.update(tick(102.0, 100.0)).unwrap();
|
||||
assert!((out - 0.02).abs() < 1e-12);
|
||||
assert!(ts.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backwardation_is_negative() {
|
||||
let mut ts = TermStructureBasis::new();
|
||||
let out = ts.update(tick(98.0, 100.0)).unwrap();
|
||||
assert!((out + 0.02).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_par_is_zero() {
|
||||
let mut ts = TermStructureBasis::new();
|
||||
assert_eq!(ts.update(tick(100.0, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(100.0 + f64::from(i % 5), 100.0))
|
||||
.collect();
|
||||
let mut a = TermStructureBasis::new();
|
||||
let mut b = TermStructureBasis::new();
|
||||
assert_eq!(
|
||||
a.batch(&ticks),
|
||||
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut ts = TermStructureBasis::new();
|
||||
ts.update(tick(102.0, 100.0));
|
||||
assert!(ts.is_ready());
|
||||
ts.reset();
|
||||
assert!(!ts.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ pub use indicators::{
|
||||
Adl, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, Alpha, AnchoredVwap, Apo, Aroon,
|
||||
AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation,
|
||||
AverageDrawdown, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Beta,
|
||||
BollingerBands, BollingerBandwidth, BollingerOutput, CalmarRatio, Camarilla,
|
||||
BollingerBands, BollingerBandwidth, BollingerOutput, CalendarSpread, CalmarRatio, Camarilla,
|
||||
CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
|
||||
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, Cmo,
|
||||
@@ -86,7 +86,7 @@ pub use indicators::{
|
||||
StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, TdCombo,
|
||||
TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure,
|
||||
TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput,
|
||||
TdSequential, TdSequentialOutput, TdSetup, Tema, ThreeInside, ThreeOutside,
|
||||
TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
|
||||
Reference in New Issue
Block a user