use super::HybridTimestamp;
use rkyv::{Archive, Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum TimeGranularity {
Daily = 0,
Weekly = 1,
BiWeekly = 2,
Monthly = 3,
Quarterly = 4,
Annual = 5,
}
impl TimeGranularity {
pub fn periods_per_year(&self) -> u32 {
match self {
TimeGranularity::Daily => 365,
TimeGranularity::Weekly => 52,
TimeGranularity::BiWeekly => 26,
TimeGranularity::Monthly => 12,
TimeGranularity::Quarterly => 4,
TimeGranularity::Annual => 1,
}
}
pub fn seasonal_lag(&self) -> u32 {
match self {
TimeGranularity::Daily => 7, TimeGranularity::Weekly => 52, TimeGranularity::BiWeekly => 26, TimeGranularity::Monthly => 12, TimeGranularity::Quarterly => 4, TimeGranularity::Annual => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum SeasonalityType {
None = 0,
Weekly = 1,
BiWeekly = 2,
Monthly = 3,
Quarterly = 4,
SemiAnnual = 5,
Annual = 6,
}
impl SeasonalityType {
pub fn period_days(&self) -> u32 {
match self {
SeasonalityType::None => 0,
SeasonalityType::Weekly => 7,
SeasonalityType::BiWeekly => 14,
SeasonalityType::Monthly => 30,
SeasonalityType::Quarterly => 91,
SeasonalityType::SemiAnnual => 182,
SeasonalityType::Annual => 365,
}
}
pub fn examples(&self) -> &'static str {
match self {
SeasonalityType::None => "No regular pattern",
SeasonalityType::Weekly => "Payroll, weekly invoicing",
SeasonalityType::BiWeekly => "Bi-weekly payroll",
SeasonalityType::Monthly => "Rent, subscriptions, utility payments",
SeasonalityType::Quarterly => "Tax payments, dividends, quarterly filings",
SeasonalityType::SemiAnnual => "Insurance premiums, bond interest",
SeasonalityType::Annual => "Year-end adjustments, depreciation",
}
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct SeasonalPattern {
pub id: Uuid,
pub account_id: u16,
pub seasonality_type: SeasonalityType,
pub period_length: u16,
pub cycle_count: u16,
pub _pad: u16,
pub confidence: f32,
pub autocorrelation_peak: f32,
pub seasonal_amplitude: f32,
pub baseline: f32,
pub trend_coefficient: f32,
pub residual_variance: f32,
pub first_observed: HybridTimestamp,
pub last_observed: HybridTimestamp,
pub flags: SeasonalPatternFlags,
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct SeasonalPatternFlags(pub u32);
impl SeasonalPatternFlags {
pub const IS_STATISTICALLY_SIGNIFICANT: u32 = 1 << 0;
pub const HAS_UPWARD_TREND: u32 = 1 << 1;
pub const HAS_DOWNWARD_TREND: u32 = 1 << 2;
pub const IS_STABLE: u32 = 1 << 3;
pub const HAS_STRUCTURAL_BREAK: u32 = 1 << 4;
}
impl SeasonalPattern {
pub fn new(account_id: u16, seasonality_type: SeasonalityType) -> Self {
Self {
id: Uuid::new_v4(),
account_id,
seasonality_type,
period_length: seasonality_type.period_days() as u16,
cycle_count: 0,
_pad: 0,
confidence: 0.0,
autocorrelation_peak: 0.0,
seasonal_amplitude: 0.0,
baseline: 0.0,
trend_coefficient: 0.0,
residual_variance: 0.0,
first_observed: HybridTimestamp::zero(),
last_observed: HybridTimestamp::zero(),
flags: SeasonalPatternFlags(0),
}
}
pub fn is_strong(&self) -> bool {
self.confidence > 0.9
}
pub fn is_significant(&self) -> bool {
self.flags.0 & SeasonalPatternFlags::IS_STATISTICALLY_SIGNIFICANT != 0
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct BehavioralBaseline {
pub id: Uuid,
pub account_id: u16,
pub period_count: u16,
pub _pad: u32,
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub mad: f64,
pub q1: f64,
pub q3: f64,
pub iqr: f64,
pub min_value: f64,
pub max_value: f64,
pub p5: f64,
pub p95: f64,
pub skewness: f64,
pub kurtosis: f64,
pub avg_transaction_count: f64,
pub transaction_count_std_dev: f64,
pub last_updated: HybridTimestamp,
pub period_start: HybridTimestamp,
pub period_end: HybridTimestamp,
}
impl BehavioralBaseline {
pub fn new(account_id: u16) -> Self {
Self {
id: Uuid::new_v4(),
account_id,
period_count: 0,
_pad: 0,
mean: 0.0,
median: 0.0,
std_dev: 0.0,
mad: 0.0,
q1: 0.0,
q3: 0.0,
iqr: 0.0,
min_value: 0.0,
max_value: 0.0,
p5: 0.0,
p95: 0.0,
skewness: 0.0,
kurtosis: 0.0,
avg_transaction_count: 0.0,
transaction_count_std_dev: 0.0,
last_updated: HybridTimestamp::zero(),
period_start: HybridTimestamp::zero(),
period_end: HybridTimestamp::zero(),
}
}
pub fn z_score(&self, value: f64) -> f64 {
if self.std_dev > 0.0 {
(value - self.mean) / self.std_dev
} else {
0.0
}
}
pub fn modified_z_score(&self, value: f64) -> f64 {
if self.mad > 0.0 {
0.6745 * (value - self.median) / self.mad
} else {
0.0
}
}
pub fn is_iqr_outlier(&self, value: f64) -> bool {
let lower_fence = self.q1 - 1.5 * self.iqr;
let upper_fence = self.q3 + 1.5 * self.iqr;
value < lower_fence || value > upper_fence
}
pub fn is_percentile_outlier(&self, value: f64) -> bool {
value < self.p5 || value > self.p95
}
pub fn is_anomaly(&self, value: f64) -> (bool, f32) {
let mut votes = 0;
let mut max_severity = 0.0f32;
let z = self.z_score(value).abs();
if z > 3.0 {
votes += 1;
max_severity = max_severity.max((z / 5.0) as f32);
}
let mz = self.modified_z_score(value).abs();
if mz > 3.5 {
votes += 1;
max_severity = max_severity.max((mz / 5.0) as f32);
}
if self.is_iqr_outlier(value) {
votes += 1;
let iqr_deviation = if value < self.q1 {
(self.q1 - value) / self.iqr
} else {
(value - self.q3) / self.iqr
};
max_severity = max_severity.max(iqr_deviation as f32);
}
if self.is_percentile_outlier(value) {
votes += 1;
}
let is_anomaly = votes >= 2;
let score = match votes {
2 => 0.5,
3 => 0.75,
4 => 1.0,
_ => 0.0,
} * max_severity.min(1.0);
(is_anomaly, score)
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct TimeSeriesMetrics {
pub id: Uuid,
pub account_id: u16,
pub period_count: u16,
pub granularity: TimeGranularity,
pub _pad: [u8; 3],
pub trend_coefficient: f64,
pub trend_intercept: f64,
pub trend_r_squared: f64,
pub volatility: f64,
pub coefficient_of_variation: f64,
pub sma: f64,
pub ema: f64,
pub current_value: f64,
pub forecasted_value: f64,
pub forecast_ci: f64,
pub period_start: HybridTimestamp,
pub flags: TimeSeriesFlags,
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct TimeSeriesFlags(pub u32);
impl TimeSeriesFlags {
pub const HAS_SIGNIFICANT_TREND: u32 = 1 << 0;
pub const IS_INCREASING: u32 = 1 << 1;
pub const IS_DECREASING: u32 = 1 << 2;
pub const IS_HIGH_VOLATILITY: u32 = 1 << 3;
pub const IS_STATIONARY: u32 = 1 << 4;
}
impl TimeSeriesMetrics {
pub fn new(account_id: u16, granularity: TimeGranularity) -> Self {
Self {
id: Uuid::new_v4(),
account_id,
period_count: 0,
granularity,
_pad: [0; 3],
trend_coefficient: 0.0,
trend_intercept: 0.0,
trend_r_squared: 0.0,
volatility: 0.0,
coefficient_of_variation: 0.0,
sma: 0.0,
ema: 0.0,
current_value: 0.0,
forecasted_value: 0.0,
forecast_ci: 0.0,
period_start: HybridTimestamp::zero(),
flags: TimeSeriesFlags(0),
}
}
pub fn is_increasing(&self) -> bool {
self.flags.0 & TimeSeriesFlags::IS_INCREASING != 0
}
pub fn is_decreasing(&self) -> bool {
self.flags.0 & TimeSeriesFlags::IS_DECREASING != 0
}
pub fn is_volatile(&self) -> bool {
self.flags.0 & TimeSeriesFlags::IS_HIGH_VOLATILITY != 0
}
}
#[derive(Debug, Clone)]
pub struct TemporalAlert {
pub id: Uuid,
pub account_id: u16,
pub alert_type: TemporalAlertType,
pub severity: f32,
pub trigger_value: f64,
pub expected_value: f64,
pub deviation: f64,
pub timestamp: HybridTimestamp,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TemporalAlertType {
Anomaly,
TrendBreak,
SeasonalDeviation,
DormantActivation,
HighVolatility,
FrequencyAnomaly,
}
impl TemporalAlertType {
pub fn icon(&self) -> &'static str {
match self {
TemporalAlertType::Anomaly => "⚠️",
TemporalAlertType::TrendBreak => "📈",
TemporalAlertType::SeasonalDeviation => "📅",
TemporalAlertType::DormantActivation => "💤",
TemporalAlertType::HighVolatility => "📊",
TemporalAlertType::FrequencyAnomaly => "🔢",
}
}
pub fn description(&self) -> &'static str {
match self {
TemporalAlertType::Anomaly => "Value outside normal range",
TemporalAlertType::TrendBreak => "Significant trend change detected",
TemporalAlertType::SeasonalDeviation => "Deviation from seasonal pattern",
TemporalAlertType::DormantActivation => "Activity on dormant account",
TemporalAlertType::HighVolatility => "Unusually high value volatility",
TemporalAlertType::FrequencyAnomaly => "Unusual transaction frequency",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_baseline_anomaly_detection() {
let mut baseline = BehavioralBaseline::new(0);
baseline.mean = 100.0;
baseline.std_dev = 10.0;
baseline.median = 100.0;
baseline.mad = 7.5;
baseline.q1 = 90.0;
baseline.q3 = 110.0;
baseline.iqr = 20.0;
baseline.p5 = 80.0;
baseline.p95 = 120.0;
let (is_anomaly, _) = baseline.is_anomaly(105.0);
assert!(!is_anomaly);
let (is_anomaly, score) = baseline.is_anomaly(200.0);
assert!(is_anomaly);
assert!(score > 0.5);
}
#[test]
fn test_z_score() {
let mut baseline = BehavioralBaseline::new(0);
baseline.mean = 100.0;
baseline.std_dev = 10.0;
let z = baseline.z_score(130.0);
assert!((z - 3.0).abs() < 0.01);
}
#[test]
fn test_seasonality_period() {
assert_eq!(SeasonalityType::Monthly.period_days(), 30);
assert_eq!(SeasonalityType::Quarterly.period_days(), 91);
assert_eq!(SeasonalityType::Annual.period_days(), 365);
}
}