greed 0.6.0

A rust tool to automate trades 📈
use crate::analysis::AssetAnalyzer;
use crate::asset::AssetSymbol;
use crate::config::platform::PlatformType;
use crate::error::GreedError;
use crate::output_format::OutputFormat;
use crate::platform::args::PlatformArgs;
use crate::platform::quote::Quote;
use crate::run::{GreedRunner, GreedRunnerArgs};
use chrono::{DateTime, Utc};
use std::ops::Range;

mod analysis;
mod assert;
pub mod asset;
mod bool;
pub mod config;
mod date;
mod db;
mod enum_display;
pub mod error;
mod fixture;
mod float;
pub mod history_range;
mod num;
pub mod output_format;
mod pager;
pub mod platform;
pub mod run;
mod statistics;
mod strategy;
mod tactic;
pub mod template;
mod trading_days;

pub async fn greed_loop(args: GreedRunnerArgs) -> Result<(), GreedError> {
    let connection = db::bootstrap()?;
    let runner = GreedRunner::from_args(args, connection).await?;
    runner.run_loop().await;
    Ok(())
}

pub async fn fetch_quote(
    assets: &[AssetSymbol],
    platform_type: &PlatformType,
    platform_args: PlatformArgs,
) -> Result<(), GreedError> {
    print_price_explainer();
    let platform = platform::for_type(platform_type, platform_args)?;
    let quotes = platform.latest_quotes(assets).await?;
    for quote in quotes {
        println!("{}", quote)
    }
    Ok(())
}

pub async fn fetch_recent_orders(
    platform_args: PlatformArgs,
    platform_type: &PlatformType,
) -> Result<(), GreedError> {
    let platform = platform::for_type(platform_type, platform_args)?;
    let orders = platform.recent_orders().await?;
    println!("Recent Orders:");
    for order in orders {
        println!("-- {}", order.display_with_time_zone(&chrono::Local))
    }
    Ok(())
}

pub async fn fetch_status(
    platform_args: PlatformArgs,
    platform_type: &PlatformType,
    show_full_status: bool,
) -> Result<(), GreedError> {
    let platform = platform::for_type(platform_type, platform_args)?;
    // Fetch account info
    let account = platform.account().await?;
    println!("Account Info: {}", account);
    // Bail if we're not showing full status
    if !show_full_status {
        return Ok(());
    }
    // Fetch Open Positions
    println!("Open Positions:");
    let positions = platform.positions().await?;
    positions
        .iter()
        .filter(|p| p.market_value.unwrap_or(0.0).abs() >= 0.01)
        .for_each(|p| println!("-- {}", p));
    // Fetch Open Orders
    println!("Open Orders:");
    let orders = platform.open_orders().await?;
    for order in &orders {
        println!("-- {}", order);
    }
    Ok(())
}

pub async fn fetch_portfolio_status_history(
    range: Range<DateTime<Utc>>,
    format: OutputFormat,
) -> Result<(), GreedError> {
    let connection = db::bootstrap()?;
    let records = db::portfolio_status::select_in_range(&connection, range)?;
    match format {
        OutputFormat::Text => {
            for record in records {
                println!("{}", record);
            }
        }
        OutputFormat::Json => {
            println!("{}", serde_json::to_string_pretty(&records)?);
        }
    }
    Ok(())
}

pub async fn fetch_stock_quote_history(
    symbols: &[AssetSymbol],
    range: Range<DateTime<Utc>>,
    format: OutputFormat,
) -> Result<(), GreedError> {
    let connection = db::bootstrap()?;
    let quotes: Vec<Quote> = db::stock_quotes::select_in_range(&connection, range)?
        .into_iter()
        .filter(|q| matches_symbol_filter(symbols, q))
        .collect();
    match format {
        OutputFormat::Text => {
            for quote in quotes {
                println!("{}", quote);
            }
        }
        OutputFormat::Json => {
            println!("{}", serde_json::to_string_pretty(&quotes)?);
        }
    }
    Ok(())
}

fn matches_symbol_filter(symbols: &[AssetSymbol], quote: &Quote) -> bool {
    symbols.is_empty() || symbols.contains(&quote.symbol)
}

fn print_price_explainer() {
    println!("ask price = lowest price where someone is willing to sell a share");
    println!("bid price = highest price someone is willing to pay for a share");
    println!("-----------");
}

pub async fn analyze_stocks(
    assets: &Vec<AssetSymbol>,
    platform_type: &PlatformType,
    platform_args: PlatformArgs,
) -> Result<(), GreedError> {
    let platform = platform::for_type(platform_type, platform_args)?;
    let analyzer = AssetAnalyzer::new(platform);
    let bars_by_symbol = analyzer.analyze_bars(assets).await?;
    for symbol in assets {
        let bars = bars_by_symbol
            .get(symbol)
            .ok_or(GreedError::new("missing bars"))?;
        println!("{}", bars)
    }
    Ok(())
}

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

    #[test]
    fn matches_symbol_filter_empty_list_matches_everything() {
        assert!(matches_symbol_filter(
            &[],
            &Quote::fixture(AssetSymbol::new("VTI"))
        ));
    }

    #[test]
    fn matches_symbol_filter_matches_listed_symbol() {
        let symbols = vec![AssetSymbol::new("VTI"), AssetSymbol::new("AAPL")];
        assert!(matches_symbol_filter(
            &symbols,
            &Quote::fixture(AssetSymbol::new("VTI"))
        ));
    }

    #[test]
    fn matches_symbol_filter_rejects_unlisted_symbol() {
        let symbols = vec![AssetSymbol::new("AAPL")];
        assert!(!matches_symbol_filter(
            &symbols,
            &Quote::fixture(AssetSymbol::new("VTI"))
        ));
    }
}