investments 4.16.1

Helps you with managing your investments
Documentation
use std::cell::RefCell;
use std::collections::{HashMap, hash_map::Entry};
use std::rc::Rc;

use log::debug;
use num_traits::FromPrimitive;

use xls_table_derive::XlsTableRow;

use crate::broker_statement::partial::{PartialBrokerStatement, PartialBrokerStatementRc};
use crate::broker_statement::trades::{ForexTrade, StockBuy, StockSell};
use crate::core::{EmptyResult, GenericResult};
use crate::currency::Cash;
use crate::forex::parse_forex_code;
use crate::formatting::format_date;
use crate::time::{Date, Time, DateTime};
use crate::types::Decimal;
use crate::util;
use crate::util::DecimalRestrictions;
use crate::xls::{self, XlsStatementParser, SectionParser, SheetReader, Cell, SkipCell, TableReader};

use super::common::{
    read_next_table_row, parse_date_cell, parse_decimal_cell, parse_quantity_cell, parse_time_cell,
    save_instrument_exchange_info};

pub type TradesRegistryRc = Rc<RefCell<HashMap<u64, bool>>>;

pub struct TradesParser {
    executed: bool,
    statement: PartialBrokerStatementRc,
    processed_trades: TradesRegistryRc,
}

impl TradesParser {
    pub fn new(
        executed: bool, statement: PartialBrokerStatementRc, processed_trades: TradesRegistryRc,
    ) -> Box<dyn SectionParser> {
        Box::new(TradesParser {executed, processed_trades, statement})
    }

    fn check_trade_id(&self, trade_id: u64) -> GenericResult<bool> {
        Ok(match self.processed_trades.borrow_mut().entry(trade_id) {
            Entry::Vacant(entry) => {
                entry.insert(self.executed);
                true
            },

            Entry::Occupied(mut entry) => {
                if self.executed {
                    let processed_executed = entry.get_mut();
                    if *processed_executed {
                        return Err!("Got a duplicated #{} trade", trade_id);
                    }
                    *processed_executed = true;
                }
                false
            },
        })
    }
}

impl SectionParser for TradesParser {
    fn parse(&mut self, parser: &mut XlsStatementParser) -> EmptyResult {
        let mut statement = self.statement.borrow_mut();

        let mut trades = xls::read_table::<TradeRow>(&mut parser.sheet)?;
        trades.sort_by_key(|trade| (trade.date, trade.time, trade.id));

        for trade in trades {
            if !self.check_trade_id(trade.id)? {
                debug!(
                    "{}: Skipping #{} trade: it's already processed for another statement.",
                    statement.get_period()?.format(), trade.id,
                );
                continue;
            }

            trade.parse(&mut statement)?;
        }

        Ok(())
    }
}

#[derive(XlsTableRow)]
struct TradeRow {
    #[column(name="Номер сделки", parse_with="parse_trade_id")]
    id: u64,
    #[column(name="Номер поручения")]
    _1: SkipCell,
    #[column(name="Признак исполнения", optional=true)]
    _2: Option<SkipCell>,
    #[column(name="Дата заключения", parse_with="parse_date_cell")]
    date: Date,
    #[column(name="Время", parse_with="parse_time_cell")]
    time: Time,
    #[column(name="Торговая площадка")]
    exchange: String,
    #[column(name="Режим торгов")]
    _6: SkipCell,
    #[column(name="Вид сделки")]
    operation: String,
    #[column(name="Сокращенное наименование", alias="Сокращенное наименование актива")]
    _8: SkipCell,
    #[column(name="Код актива")]
    symbol: String,
    #[column(name="Цена за единицу", parse_with="parse_decimal_cell")]
    price: Decimal,
    #[column(name="Валюта цены")]
    price_currency: String,
    #[column(name="Количество", parse_with="parse_quantity_cell")]
    quantity: u32,
    #[column(name="Сумма (без НКД)")]
    _13: SkipCell,
    #[column(name="НКД", parse_with="parse_decimal_cell")]
    accumulated_coupon_income: Decimal,
    #[column(name="Сумма сделки", parse_with="parse_decimal_cell")]
    volume: Decimal,
    #[column(name="Валюта расчетов")]
    settlement_currency: String,

    #[column(name="Комиссия брокера", parse_with="parse_decimal_cell")]
    commission: Decimal,
    #[column(name="Валюта комиссии")]
    commission_currency: Option<String>,

    // The following fees are actually included into brokerage commission:
    #[column(name="Комиссия биржи", optional=true)]
    _19: Option<String>,
    #[column(name="Валюта комиссии биржи", optional=true)]
    _20: Option<String>,
    #[column(name="Комиссия клир. центра", optional=true)]
    _21: Option<String>,
    #[column(name="Валюта комиссии клир. центра", optional=true)]
    _22: Option<String>,

    #[column(name="Гербовый сбор", parse_with="parse_decimal_cell", optional=true)]
    stamp_duty: Option<Decimal>,
    #[column(name="Валюта гербового сбора", optional=true)]
    stamp_duty_currency: Option<String>,

    #[column(name="Ставка РЕПО(%)")]
    leverage_rate: Option<String>,
    #[column(name="Контрагент / Брокер", alias="Контрагент")]
    _25: SkipCell,
    #[column(name="Дата расчетов", parse_with="parse_date_cell")]
    execution_date: Date,
    #[column(name="Дата поставки")]
    _27: SkipCell,
    #[column(name="Статус брокера")]
    _28: SkipCell,
    #[column(name="Тип дог.")]
    _29: SkipCell,
    #[column(name="Номер дог.")]
    _30: SkipCell,
    #[column(name="Дата дог.")]
    _31: SkipCell,
    #[column(name="Тип расчета по сделке", optional=true)]
    _32: Option<SkipCell>,
}

impl TableReader for TradeRow {
    fn next_row(sheet: &mut SheetReader) -> Option<&[Cell]> {
        read_next_table_row(sheet)
    }
}

impl TradeRow {
    fn parse(self, statement: &mut PartialBrokerStatement) -> EmptyResult {
        if !self.accumulated_coupon_income.is_zero() {
            return Err!("Bonds aren't supported yet");
        } else if self.leverage_rate.is_some() {
            return Err!("Leverage is not supported yet");
        }

        let conclusion_time = DateTime::new(self.date, self.time);
        if self.quantity == 0 {
            return Err!("Invalid {} trade quantity: {:?}", self.symbol, self.quantity);
        }

        let price = util::validate_named_cash(
            "price", &self.price_currency, self.price, DecimalRestrictions::StrictlyPositive)?;

        let volume = util::validate_named_cash(
            "trade volume", &self.settlement_currency, self.volume, DecimalRestrictions::StrictlyPositive)?;
        debug_assert_eq!(volume, (price * self.quantity).round());

        let mut commission = match self.commission_currency {
            Some(currency) => {
                util::validate_named_cash(
                    "commission amount", &currency, self.commission,
                    DecimalRestrictions::PositiveOrZero)?
            }
            None if self.commission.is_zero() => {
                Cash::new(&self.settlement_currency, self.commission)
            },
            None => return Err!(
                "Got {} trade at {} without commission currency",
                self.symbol, format_date(conclusion_time),
            ),
        };

        if let Some(amount) = self.stamp_duty {
            let currency = self.stamp_duty_currency.ok_or_else(|| format!(
                "Got {} trade with stamp duty but without stamp duty currency", self.symbol))?;

            if currency != commission.currency {
                return Err!(concat!(
                    "Got {} trade with {} stamp duty currency which differs from broker commission ",
                    "currency ({}), which is not supported yet"
                ), self.symbol, currency, commission.currency);
            }

            commission.add_assign(util::validate_named_cash(
                "stamp duty amount", &currency, amount, DecimalRestrictions::PositiveOrZero,
            )?).unwrap();
        }

        let forex = parse_forex_code(&self.symbol);

        if forex.is_err() {
            save_instrument_exchange_info(
                &mut statement.instrument_info, &self.symbol, &self.exchange)?;
        }

        match self.operation.as_str() {
            "Покупка" => {
                if let Ok((base, _quote, _lot_size)) = forex {
                    let from = volume;
                    let to = Cash::new(base, Decimal::from_u32(self.quantity).unwrap());
                    statement.forex_trades.push(ForexTrade::new(
                        conclusion_time.into(), from, to, commission));
                } else {
                    statement.stock_buys.push(StockBuy::new_trade(
                        &self.symbol, self.quantity.into(), price, volume, commission,
                        conclusion_time.into(), self.execution_date));
                }
            },
            "Продажа" => {
                if let Ok((base, _quote, _lot_size)) = forex {
                    let from = Cash::new(base, Decimal::from_u32(self.quantity).unwrap());
                    let to = volume;
                    statement.forex_trades.push(ForexTrade::new(
                        conclusion_time.into(), from, to, commission));
                } else {
                    statement.stock_sells.push(StockSell::new_trade(
                        &self.symbol, self.quantity.into(), price, volume,
                        commission, conclusion_time.into(), self.execution_date, false));
                }
            },
            _ => return Err!("Unsupported trade operation: {:?}", self.operation),
        }

        Ok(())
    }
}

fn parse_trade_id(cell: &Cell) -> GenericResult<u64> {
    let value = xls::get_string_cell(cell)?;
    Ok(value.parse().map_err(|_| format!("Got an unexpected trade ID: {:?}", value))?)
}