greed 0.5.2

A rust tool to automate trades 📈
Documentation
use crate::analysis::result::BarsResult;
use crate::bool::BooleanWhen;
use crate::config::tactic::median::MedianPeriod;
use crate::float::PercentOps;
use crate::platform::bars::Bars;
use crate::tactic::r#for::ForResult;
use crate::tactic::state::TacticState;
use crate::tactic::target::TargetAsset;
use crate::tactic::when::{WhenResult, WhenRule};
use log::{info, warn};

#[derive(Debug, Default, PartialEq)]
pub struct WhenBelowMedianRule {
    below_median_percent: f64,
    median_period: MedianPeriod,
}

impl WhenBelowMedianRule {
    pub fn boxed(below_median_percent: f64, median_period: MedianPeriod) -> Box<dyn WhenRule> {
        Box::new(Self {
            below_median_percent,
            median_period,
        })
    }

    fn is_below_median(&self, state: &TacticState, target_asset: &TargetAsset) -> bool {
        match self.median_period {
            MedianPeriod::Day => self.is_below_median_for_func(state, target_asset, |analysis| {
                &analysis.last_trading_day
            }),
            MedianPeriod::Week => {
                self.is_below_median_for_func(state, target_asset, |analysis| &analysis.seven_day)
            }
            MedianPeriod::Month => {
                self.is_below_median_for_func(state, target_asset, |analysis| &analysis.thirty_day)
            }
        }
    }

    fn is_below_median_for_func<F>(
        &self,
        state: &TacticState,
        target_asset: &TargetAsset,
        func: F,
    ) -> bool
    where
        F: Fn(&BarsResult) -> &Bars,
    {
        if self.is_state_valid(state, target_asset) {
            warn!(
                "when_below_median: state was not valid for: {}",
                target_asset.symbol
            );
            return false;
        }

        let quote = &state.quotes[&target_asset.symbol];
        if !quote.valid_ask() {
            warn!(
                "when_below_median: Ask price is not valid for: {}",
                target_asset.symbol
            );
            return false;
        }

        let analysis = &state.bar_analysis[&target_asset.symbol];
        let median = func(analysis).average_median();
        median
            .filter(Self::is_median_valid)
            .map(|m| {
                let difference_percent = quote.clone().ask_price.percent_below(m);
                (difference_percent >= self.below_median_percent)
                    .when_false(|| self.log_quote_is_not_below_median(difference_percent))
            })
            .unwrap_or(false)
    }

    fn log_quote_is_not_below_median(&self, difference_percent: f64) {
        info!(
            "when_below_median: quote was {:.2} below median, expecting {:.2}.",
            difference_percent, self.below_median_percent
        )
    }

    fn is_state_valid(&self, state: &TacticState, target_asset: &TargetAsset) -> bool {
        !state.quotes.contains_key(&target_asset.symbol)
            || !state.bar_analysis.contains_key(&target_asset.symbol)
    }

    fn is_median_valid(median: &f64) -> bool {
        *median > 0.0
    }
}

impl WhenRule for WhenBelowMedianRule {
    fn evaluate(&self, state: &TacticState, for_result: ForResult) -> WhenResult {
        let assets_below_median = for_result
            .target_assets
            .iter()
            .filter(|t| self.is_below_median(state, t))
            .map(|t| t.clone())
            .collect::<Vec<_>>();
        WhenResult {
            conditions_satisfied: !assets_below_median.is_empty(),
            target_assets: assets_below_median,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::asset::AssetSymbol;
    use crate::platform::quote::Quote;
    use std::collections::HashMap;
    use std::rc::Rc;

    #[test]
    fn evaluate_no_analysis() {
        let state = TacticState {
            bar_analysis: Rc::new(HashMap::new()),
            ..TacticState::fixture()
        };
        let rule = WhenBelowMedianRule::boxed(10.0, MedianPeriod::Day);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected = WhenResult {
            conditions_satisfied: false,
            target_assets: vec![],
        };
        assert_eq!(expected, result);
    }

    #[test]
    fn evaluate_no_quote() {
        let state = TacticState {
            quotes: HashMap::new(),
            ..TacticState::fixture()
        };
        let rule = WhenBelowMedianRule::boxed(10.0, MedianPeriod::Day);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected = WhenResult {
            conditions_satisfied: false,
            target_assets: vec![],
        };
        assert_eq!(expected, result);
    }

    #[test]
    fn evaluate_invalid_quote() {
        let spy = AssetSymbol::new("SPY");
        let quote = Quote {
            ask_price: 0.0,
            ..Default::default()
        };
        let state = TacticState {
            quotes: HashMap::from([(spy.clone(), quote)]),
            ..TacticState::fixture()
        };
        let rule = WhenBelowMedianRule::boxed(10.0, MedianPeriod::Day);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected = WhenResult {
            conditions_satisfied: false,
            target_assets: vec![],
        };
        assert_eq!(expected, result);
    }

    #[test]
    fn evaluate_no_median() {
        let spy = AssetSymbol::new("SPY");
        let bar_result = BarsResult {
            ..Default::default()
        };
        let state = TacticState {
            bar_analysis: Rc::new(HashMap::from([(spy.clone(), bar_result)])),
            ..TacticState::fixture()
        };
        let rule = WhenBelowMedianRule::boxed(10.0, MedianPeriod::Day);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected = WhenResult {
            conditions_satisfied: false,
            target_assets: vec![],
        };
        assert_eq!(expected, result);
    }

    #[test]
    fn evaluate_zero_median() {
        let spy = AssetSymbol::new("SPY");
        let bar_result = BarsResult {
            last_trading_day: Bars::with_bars(vec![Default::default()]),
            ..Default::default()
        };
        let state = TacticState {
            bar_analysis: Rc::new(HashMap::from([(spy.clone(), bar_result)])),
            ..TacticState::fixture()
        };
        let rule = WhenBelowMedianRule::boxed(10.0, MedianPeriod::Day);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected = WhenResult {
            conditions_satisfied: false,
            target_assets: vec![],
        };
        assert_eq!(expected, result);
    }

    #[test]
    fn evaluate_1d_satisfied() {
        validate_evaluation(50.0, MedianPeriod::Day, true);
    }

    #[test]
    fn evaluate_1d_not_satisfied() {
        validate_evaluation(51.0, MedianPeriod::Day, false);
    }

    #[test]
    fn evaluate_7d_satisfied() {
        validate_evaluation(33.0, MedianPeriod::Week, true);
    }

    #[test]
    fn evaluate_7d_not_satisfied() {
        validate_evaluation(34.0, MedianPeriod::Week, false);
    }

    #[test]
    fn evaluate_30d_satisfied() {
        validate_evaluation(0.0, MedianPeriod::Month, true);
    }

    #[test]
    fn evaluate_30d_not_satisfied() {
        validate_evaluation(1.0, MedianPeriod::Month, false);
    }

    fn validate_evaluation(
        below_median_percent: f64,
        median_period: MedianPeriod,
        expected_to_be_valid: bool,
    ) {
        let state = TacticState::fixture();
        let rule = WhenBelowMedianRule::boxed(below_median_percent, median_period);
        let target_assets = target_assets();
        let for_result = ForResult {
            target_assets: target_assets.clone(),
        };
        let result = rule.evaluate(&state, for_result);
        let expected_result = if expected_to_be_valid {
            WhenResult {
                conditions_satisfied: true,
                target_assets: target_assets.clone(),
            }
        } else {
            WhenResult {
                conditions_satisfied: false,
                target_assets: vec![],
            }
        };
        assert_eq!(expected_result, result);
    }

    fn target_assets() -> Vec<TargetAsset> {
        vec![
            TargetAsset::full_percent(AssetSymbol::new("SPY")),
            TargetAsset::full_percent(AssetSymbol::new("VTI")),
        ]
    }
}