libfmp 0.4.0

Async, typed Rust client for the Financial Modeling Prep API
Documentation
use libfmp::responses::technical_indicators::{
    AverageDirectionalIndexBar, DoubleExponentialMovingAverageBar, ExponentialMovingAverageBar,
    RelativeStrengthIndexBar, SimpleMovingAverageBar, StandardDeviationBar,
    TripleExponentialMovingAverageBar, WeightedMovingAverageBar, WilliamsBar,
};
use serde::{Serialize, de::DeserializeOwned};

const SMA: &[u8] = include_bytes!("fixtures/technical_indicator_sma.json");
const EMA: &[u8] = include_bytes!("fixtures/technical_indicator_ema.json");
const WMA: &[u8] = include_bytes!("fixtures/technical_indicator_wma.json");
const DEMA: &[u8] = include_bytes!("fixtures/technical_indicator_dema.json");
const TEMA: &[u8] = include_bytes!("fixtures/technical_indicator_tema.json");
const RSI: &[u8] = include_bytes!("fixtures/technical_indicator_rsi.json");
const STANDARD_DEVIATION: &[u8] =
    include_bytes!("fixtures/technical_indicator_standard_deviation.json");
const WILLIAMS: &[u8] = include_bytes!("fixtures/technical_indicator_williams.json");
const ADX: &[u8] = include_bytes!("fixtures/technical_indicator_adx.json");

#[test]
fn all_nine_exact_source_rows_round_trip_with_seven_flat_fields() {
    assert_exact::<SimpleMovingAverageBar>(SMA);
    assert_exact::<ExponentialMovingAverageBar>(EMA);
    assert_exact::<WeightedMovingAverageBar>(WMA);
    assert_exact::<DoubleExponentialMovingAverageBar>(DEMA);
    assert_exact::<TripleExponentialMovingAverageBar>(TEMA);
    assert_exact::<RelativeStrengthIndexBar>(RSI);
    assert_exact::<StandardDeviationBar>(STANDARD_DEVIATION);
    assert_exact::<WilliamsBar>(WILLIAMS);
    assert_exact::<AverageDirectionalIndexBar>(ADX);
}

fn assert_exact<T: DeserializeOwned + Serialize>(fixture: &[u8]) {
    let source: serde_json::Value = serde_json::from_slice(fixture).unwrap();
    assert_eq!(source[0].as_object().unwrap().len(), 7);
    let rows: Vec<T> = serde_json::from_value(source.clone()).unwrap();
    assert_eq!(serde_json::to_value(rows).unwrap(), source);
}

#[test]
fn source_values_preserve_second_precision_large_volume_and_negative_williams() {
    let sma: Vec<SimpleMovingAverageBar> = serde_json::from_slice(SMA).unwrap();
    assert_eq!(sma[0].date.to_string(), "2026-07-30 00:00:00");
    assert_eq!(sma[0].volume, 29_207_295);
    assert_eq!(sma[0].sma, 331.621);

    let williams: Vec<WilliamsBar> = serde_json::from_slice(WILLIAMS).unwrap();
    assert_eq!(williams[0].williams, -48.29500396510714);

    let adx: Vec<AverageDirectionalIndexBar> = serde_json::from_slice(ADX).unwrap();
    assert_eq!(adx[0].adx, 34.69458756515438);
}

#[test]
fn every_documented_field_is_required_non_null_and_unknowns_are_accepted() {
    assert_contract::<SimpleMovingAverageBar>(SMA);
    assert_contract::<ExponentialMovingAverageBar>(EMA);
    assert_contract::<WeightedMovingAverageBar>(WMA);
    assert_contract::<DoubleExponentialMovingAverageBar>(DEMA);
    assert_contract::<TripleExponentialMovingAverageBar>(TEMA);
    assert_contract::<RelativeStrengthIndexBar>(RSI);
    assert_contract::<StandardDeviationBar>(STANDARD_DEVIATION);
    assert_contract::<WilliamsBar>(WILLIAMS);
    assert_contract::<AverageDirectionalIndexBar>(ADX);
}

fn assert_contract<T: DeserializeOwned>(fixture: &[u8]) {
    let source: serde_json::Value = serde_json::from_slice(fixture).unwrap();
    let keys = source[0]
        .as_object()
        .unwrap()
        .keys()
        .cloned()
        .collect::<Vec<_>>();
    for key in keys {
        let mut missing = source.clone();
        missing[0].as_object_mut().unwrap().remove(&key);
        assert!(
            serde_json::from_value::<Vec<T>>(missing).is_err(),
            "accepted missing {key}"
        );

        let mut null = source.clone();
        null[0][&key] = serde_json::Value::Null;
        assert!(
            serde_json::from_value::<Vec<T>>(null).is_err(),
            "accepted null {key}"
        );
    }

    let mut forward = source;
    forward[0]["futureProviderField"] = serde_json::json!({ "nested": [1, true, null] });
    assert_eq!(serde_json::from_value::<Vec<T>>(forward).unwrap().len(), 1);
}

#[test]
fn every_contract_is_a_bare_array_preserving_empty_and_multiple_rows() {
    assert_bare_array::<SimpleMovingAverageBar>(SMA);
    assert_bare_array::<ExponentialMovingAverageBar>(EMA);
    assert_bare_array::<WeightedMovingAverageBar>(WMA);
    assert_bare_array::<DoubleExponentialMovingAverageBar>(DEMA);
    assert_bare_array::<TripleExponentialMovingAverageBar>(TEMA);
    assert_bare_array::<RelativeStrengthIndexBar>(RSI);
    assert_bare_array::<StandardDeviationBar>(STANDARD_DEVIATION);
    assert_bare_array::<WilliamsBar>(WILLIAMS);
    assert_bare_array::<AverageDirectionalIndexBar>(ADX);
    assert!(
        serde_json::from_value::<Vec<SimpleMovingAverageBar>>(serde_json::json!({ "sma": [] }))
            .is_err()
    );
}

fn assert_bare_array<T: DeserializeOwned>(fixture: &[u8]) {
    assert!(serde_json::from_slice::<Vec<T>>(b"[]").unwrap().is_empty());
    let mut source: serde_json::Value = serde_json::from_slice(fixture).unwrap();
    let duplicate = source[0].clone();
    source.as_array_mut().unwrap().push(duplicate);
    assert_eq!(serde_json::from_value::<Vec<T>>(source).unwrap().len(), 2);
}

#[test]
fn volume_preserves_values_above_u32_and_float_metrics_accept_integers_and_fractions() {
    let mut source: serde_json::Value = serde_json::from_slice(SMA).unwrap();
    source[0]["volume"] = serde_json::json!(u64::MAX);
    source[0]["sma"] = serde_json::json!(7);
    let integer: Vec<SimpleMovingAverageBar> = serde_json::from_value(source).unwrap();
    assert_eq!(integer[0].volume, u64::MAX);
    assert_eq!(integer[0].sma, 7.0);

    let mut rsi: serde_json::Value = serde_json::from_slice(RSI).unwrap();
    rsi[0]["rsi"] = serde_json::json!(59.25);
    assert_eq!(
        serde_json::from_value::<Vec<RelativeStrengthIndexBar>>(rsi).unwrap()[0].rsi,
        59.25
    );

    let mut williams: serde_json::Value = serde_json::from_slice(WILLIAMS).unwrap();
    williams[0]["williams"] = serde_json::json!(-100);
    assert_eq!(
        serde_json::from_value::<Vec<WilliamsBar>>(williams).unwrap()[0].williams,
        -100.0
    );
}

#[test]
fn metric_keys_are_distinct_and_exactly_cased() {
    let fixtures = [
        (SMA, "sma", "SMA"),
        (EMA, "ema", "EMA"),
        (WMA, "wma", "WMA"),
        (DEMA, "dema", "DEMA"),
        (TEMA, "tema", "TEMA"),
        (RSI, "rsi", "RSI"),
        (
            STANDARD_DEVIATION,
            "standardDeviation",
            "standard_deviation",
        ),
        (WILLIAMS, "williams", "Williams"),
        (ADX, "adx", "ADX"),
    ];
    for (fixture, exact, wrong) in fixtures {
        let source: serde_json::Value = serde_json::from_slice(fixture).unwrap();
        assert!(source[0].get(exact).is_some());
        assert!(source[0].get(wrong).is_none());
    }

    let ema_source: serde_json::Value = serde_json::from_slice(EMA).unwrap();
    assert!(serde_json::from_value::<Vec<SimpleMovingAverageBar>>(ema_source).is_err());

    let mut standard: serde_json::Value = serde_json::from_slice(STANDARD_DEVIATION).unwrap();
    let value = standard[0]
        .as_object_mut()
        .unwrap()
        .remove("standardDeviation")
        .unwrap();
    standard[0]["standard_deviation"] = value;
    assert!(serde_json::from_value::<Vec<StandardDeviationBar>>(standard).is_err());
}

#[test]
fn timestamps_require_the_documented_naive_seconds_shape() {
    for invalid in ["2026-07-30 00:00", "2026-07-30T00:00:00Z"] {
        let mut source: serde_json::Value = serde_json::from_slice(SMA).unwrap();
        source[0]["date"] = serde_json::json!(invalid);
        assert!(serde_json::from_value::<Vec<SimpleMovingAverageBar>>(source).is_err());
    }
}