finalytics 0.8.9

A rust library for financial data analysis
Documentation
use polars::prelude::*;
use std::error::Error;
use chrono::{DateTime, NaiveDateTime};
use crate::analytics::technicals::TechnicalIndicators;
use crate::analytics::optimization::{ObjectiveFunction, portfolio_optimization, Constraints, filter_constraints};
use crate::analytics::statistics::{PerformanceStats, covariance_matrix, daily_portfolio_returns};
use crate::prelude::{Column, TickersData, IntervalDays, Tickers, Ticker};
use crate::utils::date_utils::interval_days;

#[derive(Debug, Clone)]
pub struct TickerPerformanceStats {
    pub ticker_symbol: String,
    pub benchmark_symbol: String,
    pub start_date: String,
    pub end_date: String,
    pub dates_array: Vec<String>,
    pub interval: IntervalDays,
    pub confidence_level: f64,
    pub risk_free_rate: f64,
    pub security_prices: Series,
    pub security_returns: Series,
    pub benchmark_returns: Series,
    pub performance_stats: PerformanceStats,
}

pub trait TickerPerformance {
    fn performance_stats(&self) -> impl std::future::Future<Output = Result<TickerPerformanceStats, Box<dyn Error>>>;
}

impl TickerPerformance for Ticker {
    /// Computes the performance statistics for the ticker
    ///
    /// # Returns
    ///
    /// * `TickerPerformanceStats` struct
    async fn performance_stats(&self) -> Result<TickerPerformanceStats, Box<dyn Error>> {
        let security_df = self.roc(1, Some(Column::AdjClose)).await?;
        let security_prices = security_df.column(Column::AdjClose.as_str())?.as_series().unwrap();
        let security_returns = DataFrame::new(vec![
            security_df.column("timestamp")?.clone(),
            security_df.column("roc-1")?.clone().with_name(self.ticker.as_str().into())
        ])?;
        let benchmark_returns = self.benchmark_ticker.clone().unwrap().roc(1, Some(Column::AdjClose)).await?;
        let benchmark_returns = security_returns.join(
            &benchmark_returns,
            ["timestamp"],
            ["timestamp"],
            JoinArgs::new(JoinType::Left),
            None
        )?;
        let benchmark_returns = benchmark_returns.sort(["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
        let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Forward(None))?;
        let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Backward(None))?;
        let dates_array = benchmark_returns.column("timestamp")?.datetime()?
            .into_no_null_iter().map(|x| DateTime::from_timestamp_millis(x).unwrap()
            .naive_local()).collect::<Vec<NaiveDateTime>>();
        let interval = interval_days(dates_array.clone());
        let dates_array = dates_array.iter().map(|x| x.to_string()).collect::<Vec<String>>();
        let security_returns = benchmark_returns.column(&self.ticker)?.as_series().unwrap();
        let benchmark_returns = benchmark_returns.column("roc-1")?.as_series().unwrap();

        let performance_stats = PerformanceStats::compute_stats(
            security_returns.clone(), benchmark_returns.clone(),
            self.risk_free_rate, self.confidence_level, interval)?;
        Ok(TickerPerformanceStats {
            ticker_symbol: self.ticker.clone(),
            benchmark_symbol: self.benchmark_symbol.clone(),
            start_date: self.start_date.clone(),
            end_date: self.end_date.clone(),
            dates_array,
            interval,
            confidence_level: self.confidence_level,
            risk_free_rate: self.risk_free_rate,
            security_prices: security_prices.clone(),
            security_returns: security_returns.clone(),
            benchmark_returns: benchmark_returns.clone(),
            performance_stats
        })
    }

}

/// # Portfolio Performance Struct
/// Helps compute the performance statistics for a portfolio
#[derive(Debug, Clone)]
pub struct PortfolioPerformanceStats {
    pub ticker_symbols: Vec<String>,
    pub benchmark_symbol: String,
    pub start_date: String,
    pub end_date: String,
    pub interval: IntervalDays,
    pub dates_array: Vec<String>,
    pub confidence_level: f64,
    pub risk_free_rate: f64,
    pub portfolio_returns: DataFrame,
    pub benchmark_returns: Series,
    pub objective_function: ObjectiveFunction,
    pub optimization_method: String,
    pub constraints: Constraints,
    pub optimal_weights: Vec<f64>,
    pub category_weights: Option<Vec<(String, String, f64)>>,
    pub optimal_portfolio_returns: Series,
    pub performance_stats: PerformanceStats,
    pub efficient_frontier: Option<Vec<Vec<f64>>>,
}


impl PortfolioPerformanceStats {
    /// Computes the performance statistics for the portfolio
    ///
    /// # Returns
    ///
    /// * `PortfolioPerformanceStats` struct
    #[allow(clippy::too_many_arguments)]
    pub async fn performance_stats(
        tickers: Tickers,
        benchmark_ticker: Ticker,
        start_date: &str,
        end_date: &str,
        confidence_level: f64,
        risk_free_rate: f64,
        objective_function: ObjectiveFunction,
        constraints: Option<Constraints>,
        weights: Option<Vec<f64>>,
    ) -> Result<PortfolioPerformanceStats, Box<dyn Error>> {
        let ticker_symbols = tickers.tickers.clone().iter().map(|x| x.ticker.clone()).collect::<Vec<String>>();
        let benchmark_symbol = benchmark_ticker.ticker.clone();
        let mut portfolio_returns = tickers.returns().await?;
        let portfolio_dates = portfolio_returns
            .column("timestamp").unwrap()
            .str().unwrap()
            .into_no_null_iter()
            .map(|x| NaiveDateTime::parse_from_str(x, "%Y-%m-%d %H:%M:%S").unwrap())
            .collect::<Vec<NaiveDateTime>>();
        let _ = portfolio_returns.drop_in_place("timestamp")?;
        let _=  portfolio_returns.insert_column(0, Series::new("timestamp".into(), portfolio_dates))?;

        let benchmark_returns = benchmark_ticker.roc(1, Some(Column::AdjClose)).await?;
        let benchmark_returns =  portfolio_returns.join(
            &benchmark_returns,
            ["timestamp"],
            ["timestamp"],
            JoinArgs::new(JoinType::Left),
            None
        )?;

        let benchmark_returns = benchmark_returns.sort(["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
        let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Zero)?;
        let dates_array = benchmark_returns.column("timestamp")?.datetime()?
            .into_no_null_iter().map(|x| DateTime::from_timestamp_millis(x).unwrap()
            .naive_local()).collect::<Vec<NaiveDateTime>>();
        let interval = interval_days(dates_array.clone());
        let dates_array = dates_array.iter().map(|x| x.to_string()).collect::<Vec<String>>();
        let benchmark_returns = benchmark_returns.column("roc-1")?.as_series().unwrap();

        let _ = portfolio_returns.drop_in_place("timestamp")?;

        let fetched_symbols = portfolio_returns.get_column_names().iter().map(|x| x.to_string()).collect::<Vec<String>>();

        let constraints = filter_constraints(constraints.clone(), ticker_symbols.clone(), fetched_symbols.clone());

        let mean_returns = portfolio_returns
            .clone()
            .get_columns()
            .iter()
            .map(|col| {
                col.f64()
                    .unwrap()
                    .mean()
                    .unwrap()
            })
            .collect::<Vec<f64>>();
        let cov_matrix = covariance_matrix(&portfolio_returns)?;

        let (efficient_frontier, optimal_weights, category_weights) = if let Some(weights) = weights {
            // If weights are provided, use them directly
            if weights.len() != fetched_symbols.len() {
                let fetched_symbols_set: std::collections::HashSet<_> = fetched_symbols.iter().collect();
                let requested_symbols_set: std::collections::HashSet<_> = ticker_symbols.iter().collect();
                let not_fetched_symbols: Vec<_> = requested_symbols_set.difference(&fetched_symbols_set)
                    .cloned().collect();
                return if !not_fetched_symbols.is_empty() {
                    Err(Box::from(format!(
                        "The following ticker symbols were not fetched: {not_fetched_symbols:?}. Please provide new weights for the available symbols."
                    )))
                } else {
                    Err(Box::from("Weights length must match the number of symbols"))
                }
            }
            (None, weights, None)
        } else {
            // If no weights are provided, use the optimization method to find optimal weights
            let opt_result = portfolio_optimization(&mean_returns, &cov_matrix, &portfolio_returns, risk_free_rate,
                                                     confidence_level, objective_function, constraints.clone());
            (Some(opt_result.efficient_frontier), opt_result.optimal_weights, Some(opt_result.category_weights))
        };

        let daily_portfolio_returns = daily_portfolio_returns(&optimal_weights, &portfolio_returns);

        let performance_stats = PerformanceStats::compute_stats(
            daily_portfolio_returns.clone(), benchmark_returns.clone(),
            risk_free_rate, confidence_level, interval)?;


        Ok(PortfolioPerformanceStats{
            ticker_symbols: fetched_symbols.clone(),
            benchmark_symbol: benchmark_symbol.to_string(),
            start_date: start_date.to_string(),
            end_date: end_date.to_string(),
            interval,
            dates_array: dates_array.clone(),
            confidence_level,
            risk_free_rate,
            portfolio_returns: portfolio_returns.clone(),
            benchmark_returns: benchmark_returns.clone(),
            objective_function,
            optimization_method: "COBYLA".to_string(),
            constraints,
            optimal_weights,
            category_weights,
            optimal_portfolio_returns: daily_portfolio_returns.clone(),
            performance_stats,
            efficient_frontier,
        })
    }
}