bitcoin-heuristics 0.1.0

Pattern detection heuristics for Bitcoin on-chain analytics — consolidations, distributions, CoinJoin, fee spikes, dormant supply reactivation and more
Documentation
use crate::models::{IoScaleTier, StructuralPattern};

pub fn analyze_topology(input_count: i32, output_count: i32) -> (StructuralPattern, IoScaleTier) {
    let total_io = input_count + output_count;

    let scale = if total_io <= 2 {
        IoScaleTier::Minimal
    } else if total_io <= 4 {
        IoScaleTier::Basic
    } else if total_io <= 10 {
        IoScaleTier::Low
    } else if total_io <= 25 {
        IoScaleTier::Medium
    } else if total_io <= 50 {
        IoScaleTier::High
    } else if total_io <= 100 {
        IoScaleTier::Institutional
    } else if total_io <= 500 {
        IoScaleTier::Wholesale
    } else {
        IoScaleTier::Massive
    };

    let is_consolidation = input_count >= 3 && output_count <= 2;
    let is_distribution = input_count <= 2 && output_count >= 5;
    let is_simple_send = input_count <= 2 && output_count <= 2;
    let is_sweep = input_count >= 2 && output_count == 1;
    let is_extreme_fan_out = input_count <= 2 && output_count >= 20;
    let is_extreme_fan_in = input_count >= 20 && output_count <= 3;
    let is_symmetric_complex = input_count > 3
        && output_count > 3
        && (input_count as f64 / output_count as f64).abs() < 2.0;

    let pattern = if is_extreme_fan_out {
        StructuralPattern::ExtremeFanOut
    } else if is_extreme_fan_in {
        StructuralPattern::ExtremeFanIn
    } else if is_sweep {
        StructuralPattern::TotalSweep
    } else if is_distribution {
        StructuralPattern::Distribution
    } else if is_consolidation {
        StructuralPattern::Consolidation
    } else if is_symmetric_complex {
        StructuralPattern::SymmetricComplex
    } else if is_simple_send {
        StructuralPattern::SimpleSend
    } else {
        StructuralPattern::AsymmetricComplex
    };

    (pattern, scale)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_send() {
        let (pattern, scale) = analyze_topology(1, 2);
        assert_eq!(pattern, StructuralPattern::SimpleSend);
        assert_eq!(scale, IoScaleTier::Basic);
    }

    #[test]
    fn test_consolidation() {
        // input>=3, output<=2 → Consolidation, but sweep (output==1) takes priority
        let (pattern, _) = analyze_topology(10, 1);
        assert_eq!(pattern, StructuralPattern::TotalSweep);
    }

    #[test]
    fn test_extreme_fan_in() {
        let (pattern, _) = analyze_topology(25, 2);
        assert_eq!(pattern, StructuralPattern::ExtremeFanIn);
    }

    #[test]
    fn test_extreme_fan_out() {
        // total_io = 1 + 50 = 51 → Institutional scale (50 < 51 <= 100)
        let (pattern, scale) = analyze_topology(1, 50);
        assert_eq!(pattern, StructuralPattern::ExtremeFanOut);
        assert_eq!(scale, IoScaleTier::Institutional);
    }

    #[test]
    fn test_sweep() {
        let (pattern, _) = analyze_topology(5, 1);
        assert_eq!(pattern, StructuralPattern::TotalSweep);
    }

    #[test]
    fn test_scale_massive() {
        let (_, scale) = analyze_topology(300, 300);
        assert_eq!(scale, IoScaleTier::Massive);
    }
}