use rust_decimal::prelude::ToPrimitive;
use rust_decimal::{Decimal, RoundingStrategy};
use crate::domain::GreeksAttributionRow;
use crate::engine::{
AttributionSubstrate, LegAttributionSample, StepAttributionScalars, UnitGreeks,
};
use crate::error::BacktestError;
const NANOS_PER_DAY: i64 = 86_400_000_000_000;
const DOLLARS_TO_CENTS: i64 = 100;
const IV_DECIMAL_TO_PP: i64 = 100;
pub const RESIDUAL_WARN_FLOOR_CENTS: i64 = 100;
#[must_use = "the attribution rows are the analytics output and must be used"]
pub fn attribute(
substrate: &AttributionSubstrate,
) -> Result<Vec<GreeksAttributionRow>, BacktestError> {
let mut rows = Vec::with_capacity(substrate.steps.len());
let mut offset: usize = 0;
for step in &substrate.steps {
let end = offset
.checked_add(step.leg_count)
.ok_or(BacktestError::ArithmeticOverflow)?;
let legs = substrate.legs.get(offset..end).ok_or_else(|| {
BacktestError::Bundle(format!(
"attribution substrate leg slice {offset}..{end} is out of range for step {}",
step.step
))
})?;
offset = end;
let row = attribute_step(step, legs)?;
if residual_is_notable(row.residual_cents, step.step_pnl_cents) {
tracing::warn!(
step = step.step,
residual_cents = row.residual_cents,
step_pnl_cents = step.step_pnl_cents,
"attribution residual exceeds the advisory model-quality threshold"
);
}
rows.push(row);
}
Ok(rows)
}
fn attribute_step(
step: &StepAttributionScalars,
legs: &[LegAttributionSample],
) -> Result<GreeksAttributionRow, BacktestError> {
let delta_t_days = step_delta_t_days(step)?;
let delta_s_cents = step_delta_s_cents(step)?;
let mut theta_dec = Decimal::ZERO;
let mut delta_dec = Decimal::ZERO;
let mut vega_dec = Decimal::ZERO;
for leg in legs {
let Some(greeks) = leg.prior_greeks else {
continue;
};
let signed_weight = signed_weight(leg)?;
let theta_leg = mul4(
greeks.theta,
delta_t_days,
Decimal::from(DOLLARS_TO_CENTS),
signed_weight,
)?;
theta_dec = theta_dec
.checked_add(theta_leg)
.ok_or(BacktestError::ArithmeticOverflow)?;
let delta_leg = mul3(greeks.delta, delta_s_cents, signed_weight)?;
delta_dec = delta_dec
.checked_add(delta_leg)
.ok_or(BacktestError::ArithmeticOverflow)?;
let delta_iv_pp = leg_delta_iv_pp(leg, greeks)?;
let vega_leg = mul4(
greeks.vega,
delta_iv_pp,
Decimal::from(DOLLARS_TO_CENTS),
signed_weight,
)?;
vega_dec = vega_dec
.checked_add(vega_leg)
.ok_or(BacktestError::ArithmeticOverflow)?;
}
let theta_cents = round_to_cents(theta_dec)?;
let delta_cents = round_to_cents(delta_dec)?;
let vega_cents = round_to_cents(vega_dec)?;
let spread_capture_cents = step.spread_capture_cents;
let fees_cents = step.fees_cents;
let attributed = theta_cents
.checked_add(delta_cents)
.and_then(|s| s.checked_add(vega_cents))
.and_then(|s| s.checked_add(spread_capture_cents))
.and_then(|s| s.checked_sub(fees_cents))
.ok_or(BacktestError::ArithmeticOverflow)?;
let residual_cents = step
.step_pnl_cents
.checked_sub(attributed)
.ok_or(BacktestError::ArithmeticOverflow)?;
Ok(GreeksAttributionRow::new(
step.step,
step.ts_ns,
theta_cents,
delta_cents,
vega_cents,
spread_capture_cents,
fees_cents,
residual_cents,
))
}
fn step_delta_t_days(step: &StepAttributionScalars) -> Result<Decimal, BacktestError> {
let diff = step
.ts_ns
.checked_sub(step.prior_ts_ns)
.ok_or(BacktestError::ArithmeticOverflow)?;
Decimal::from(diff)
.checked_div(Decimal::from(NANOS_PER_DAY))
.ok_or(BacktestError::ArithmeticOverflow)
}
fn step_delta_s_cents(step: &StepAttributionScalars) -> Result<Decimal, BacktestError> {
let now =
i64::try_from(step.underlying_cents).map_err(|_| BacktestError::ArithmeticOverflow)?;
let prior = i64::try_from(step.prior_underlying_cents)
.map_err(|_| BacktestError::ArithmeticOverflow)?;
let diff = now
.checked_sub(prior)
.ok_or(BacktestError::ArithmeticOverflow)?;
Ok(Decimal::from(diff))
}
fn leg_delta_iv_pp(
leg: &LegAttributionSample,
greeks: UnitGreeks,
) -> Result<Decimal, BacktestError> {
leg.current_iv
.checked_sub(greeks.implied_volatility)
.and_then(|d| d.checked_mul(Decimal::from(IV_DECIMAL_TO_PP)))
.ok_or(BacktestError::ArithmeticOverflow)
}
fn signed_weight(leg: &LegAttributionSample) -> Result<Decimal, BacktestError> {
let weight = u64::from(leg.quantity)
.checked_mul(u64::from(leg.contract_multiplier))
.ok_or(BacktestError::ArithmeticOverflow)?;
let weight = Decimal::from(weight);
Ok(match sign(leg.side) {
Sign::Positive => weight,
Sign::Negative => -weight,
})
}
fn sign(side: optionstratlib::Side) -> Sign {
match side {
optionstratlib::Side::Long => Sign::Positive,
optionstratlib::Side::Short => Sign::Negative,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sign {
Positive,
Negative,
}
fn mul3(a: Decimal, b: Decimal, c: Decimal) -> Result<Decimal, BacktestError> {
a.checked_mul(b)
.and_then(|ab| ab.checked_mul(c))
.ok_or(BacktestError::ArithmeticOverflow)
}
fn mul4(a: Decimal, b: Decimal, c: Decimal, d: Decimal) -> Result<Decimal, BacktestError> {
mul3(a, b, c)?
.checked_mul(d)
.ok_or(BacktestError::ArithmeticOverflow)
}
fn round_to_cents(value: Decimal) -> Result<i64, BacktestError> {
value
.round_dp_with_strategy(0, RoundingStrategy::MidpointNearestEven)
.to_i64()
.ok_or(BacktestError::ArithmeticOverflow)
}
#[must_use]
pub fn residual_is_notable(residual_cents: i64, step_pnl_cents: i64) -> bool {
let residual = residual_cents.unsigned_abs();
let target = step_pnl_cents.unsigned_abs();
let floor = RESIDUAL_WARN_FLOOR_CENTS.unsigned_abs();
residual > floor && residual > target
}
#[cfg(test)]
mod tests {
use optionstratlib::Side;
use rust_decimal_macros::dec;
use super::{attribute, residual_is_notable};
use crate::domain::GreeksAttributionRow;
use crate::engine::{
AttributionSubstrate, LegAttributionSample, StepAttributionScalars, UnitGreeks,
};
const TS0: i64 = 1_750_291_200_000_000_000;
const NANOS_PER_DAY: i64 = 86_400_000_000_000;
fn greeks(
delta: rust_decimal::Decimal,
theta: rust_decimal::Decimal,
vega: rust_decimal::Decimal,
iv: rust_decimal::Decimal,
) -> UnitGreeks {
UnitGreeks {
delta,
gamma: dec!(0),
theta,
vega,
implied_volatility: iv,
}
}
fn leg(
prior: Option<UnitGreeks>,
current_iv: rust_decimal::Decimal,
quantity: u32,
multiplier: u32,
side: Side,
) -> LegAttributionSample {
LegAttributionSample {
prior_greeks: prior,
current_iv,
quantity,
contract_multiplier: multiplier,
side,
}
}
#[allow(clippy::too_many_arguments)]
fn one_step(
ts_ns: i64,
prior_ts_ns: i64,
underlying: u64,
prior_underlying: u64,
spread_capture: i64,
fees: i64,
step_pnl: i64,
legs: Vec<LegAttributionSample>,
) -> AttributionSubstrate {
let scalars = StepAttributionScalars {
step: 0,
ts_ns,
prior_ts_ns,
underlying_cents: underlying,
prior_underlying_cents: prior_underlying,
spread_capture_cents: spread_capture,
fees_cents: fees,
step_pnl_cents: step_pnl,
leg_count: legs.len(),
};
AttributionSubstrate {
steps: vec![scalars],
legs,
}
}
fn identity_holds(row: &GreeksAttributionRow, step_pnl: i64) -> bool {
row.theta_pnl_cents + row.delta_pnl_cents + row.vega_pnl_cents + row.spread_capture_cents
- row.fees_cents
+ row.residual_cents
== step_pnl
}
#[test]
fn test_attribute_step_zero_worked_reconciliation_matches_doc() {
let substrate = one_step(
TS0,
TS0, 500_000,
500_000, -60,
520,
1_480,
vec![leg(None, dec!(0.20), 4, 100, Side::Short)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("attribution succeeds");
};
let Some(row) = rows.first() else {
panic!("one attribution row");
};
assert_eq!(row.theta_pnl_cents, 0);
assert_eq!(row.delta_pnl_cents, 0);
assert_eq!(row.vega_pnl_cents, 0);
assert_eq!(row.spread_capture_cents, -60);
assert_eq!(row.fees_cents, 520);
assert_eq!(
row.residual_cents, 2_060,
"residual closes the step-0 baseline"
);
assert!(identity_holds(row, 1_480));
}
#[test]
fn test_attribute_single_theta_driver_uses_daily_greek_and_day_delta() {
let substrate = one_step(
TS0 + 2 * NANOS_PER_DAY,
TS0,
500_000,
500_000,
0,
0,
1_000,
vec![leg(
Some(greeks(dec!(0), dec!(-0.05), dec!(0), dec!(0.20))),
dec!(0.20),
1,
100,
Side::Short,
)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("attribution succeeds");
};
let Some(row) = rows.first() else {
panic!("one row");
};
assert_eq!(row.theta_pnl_cents, 1_000);
assert_eq!(row.delta_pnl_cents, 0);
assert_eq!(row.vega_pnl_cents, 0);
assert_eq!(
row.residual_cents, 0,
"step_pnl matches the theta term exactly"
);
assert!(identity_holds(row, 1_000));
}
#[test]
fn test_attribute_single_delta_driver_uses_per_dollar_ratio() {
let substrate = one_step(
TS0,
TS0,
500_100,
500_000,
0,
0,
5_000,
vec![leg(
Some(greeks(dec!(0.5), dec!(0), dec!(0), dec!(0.20))),
dec!(0.20),
1,
100,
Side::Long,
)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("attribution succeeds");
};
let Some(row) = rows.first() else {
panic!("one row");
};
assert_eq!(row.delta_pnl_cents, 5_000);
assert_eq!(row.theta_pnl_cents, 0);
assert_eq!(row.vega_pnl_cents, 0);
assert_eq!(row.residual_cents, 0);
assert!(identity_holds(row, 5_000));
}
#[test]
fn test_attribute_single_vega_driver_uses_per_point_greek_and_pp_delta() {
let substrate = one_step(
TS0,
TS0,
500_000,
500_000,
0,
0,
1_000,
vec![leg(
Some(greeks(dec!(0), dec!(0), dec!(0.1), dec!(0.20))),
dec!(0.21),
1,
100,
Side::Long,
)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("attribution succeeds");
};
let Some(row) = rows.first() else {
panic!("one row");
};
assert_eq!(row.vega_pnl_cents, 1_000);
assert_eq!(row.theta_pnl_cents, 0);
assert_eq!(row.delta_pnl_cents, 0);
assert_eq!(row.residual_cents, 0);
assert!(identity_holds(row, 1_000));
}
#[test]
fn test_attribute_weights_quantity_once_not_squared() {
let substrate = one_step(
TS0,
TS0,
500_100,
500_000,
0,
0,
15_000,
vec![leg(
Some(greeks(dec!(0.5), dec!(0), dec!(0), dec!(0.20))),
dec!(0.20),
3,
100,
Side::Long,
)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("attribution succeeds");
};
let Some(row) = rows.first() else {
panic!("one row");
};
assert_eq!(
row.delta_pnl_cents, 15_000,
"quantity weighted once, not squared"
);
assert!(identity_holds(row, 15_000));
}
#[test]
fn test_attribute_short_leg_greek_sign_is_opposite_a_long() {
let long = one_step(
TS0,
TS0,
500_100,
500_000,
0,
0,
5_000,
vec![leg(
Some(greeks(dec!(0.5), dec!(0), dec!(0), dec!(0.20))),
dec!(0.20),
1,
100,
Side::Long,
)],
);
let short = one_step(
TS0,
TS0,
500_100,
500_000,
0,
0,
-5_000,
vec![leg(
Some(greeks(dec!(0.5), dec!(0), dec!(0), dec!(0.20))),
dec!(0.20),
1,
100,
Side::Short,
)],
);
let (Ok(long_rows), Ok(short_rows)) = (attribute(&long), attribute(&short)) else {
panic!("both runs attribute");
};
let (Some(long_row), Some(short_row)) = (long_rows.first(), short_rows.first()) else {
panic!("one row each");
};
assert_eq!(long_row.delta_pnl_cents, 5_000);
assert_eq!(short_row.delta_pnl_cents, -5_000);
}
#[test]
fn test_attribute_large_residual_is_reported_never_an_error() {
let substrate = one_step(
TS0,
TS0,
500_100,
500_000, 0,
0,
100, vec![leg(
Some(greeks(dec!(0.5), dec!(0), dec!(0), dec!(0.20))),
dec!(0.20),
1,
100,
Side::Long,
)],
);
let Ok(rows) = attribute(&substrate) else {
panic!("a large residual never errors the pass");
};
let Some(row) = rows.first() else {
panic!("one row");
};
assert_eq!(row.delta_pnl_cents, 5_000);
assert_eq!(
row.residual_cents, -4_900,
"residual closes the identity exactly"
);
assert!(identity_holds(row, 100));
assert!(
residual_is_notable(row.residual_cents, 100),
"|residual| 4_900 exceeds both the floor and |step_pnl| 100 → advisory notable"
);
}
#[test]
fn test_residual_is_notable_only_above_floor_and_step_pnl() {
assert!(!residual_is_notable(50, 0));
assert!(!residual_is_notable(500, 10_000));
assert!(residual_is_notable(20_000, 10_000));
}
#[test]
fn test_attribute_is_deterministic_for_same_substrate() {
let substrate = one_step(
TS0 + NANOS_PER_DAY,
TS0,
500_150,
500_000,
-12,
130,
4_321,
vec![leg(
Some(greeks(dec!(0.4), dec!(-0.05), dec!(0.1), dec!(0.20))),
dec!(0.205),
2,
100,
Side::Short,
)],
);
let (Ok(a), Ok(b)) = (attribute(&substrate), attribute(&substrate)) else {
panic!("both attributions succeed");
};
assert_eq!(a, b, "same substrate ⇒ byte-identical rows");
}
}