Skip to content

Commit

Permalink
Make simulate-sell to work for portfolios containing symbols for whic…
Browse files Browse the repository at this point in the history
…h we have no available quotes if we have net asset value information from statement (#40, #64)
  • Loading branch information
KonishchevDmitry committed Apr 10, 2022
1 parent 391360d commit f3a0ff1
Show file tree
Hide file tree
Showing 14 changed files with 68 additions and 44 deletions.
1 change: 1 addition & 0 deletions check
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ main() {
new_without_default
redundant_field_names
too_many_arguments
type-complexity
unit_arg
'
check '--bins --examples' "$blacklist"
Expand Down
6 changes: 3 additions & 3 deletions src/analysis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ pub fn analyse(
}

statistics.process(|statistics| {
let cash_assets = statement.cash_assets.total_assets_real_time(
let cash_assets = statement.assets.cash.total_assets_real_time(
&statistics.currency, &converter)?;

Ok(statistics.add_assets(broker, "Cash", cash_assets))
})?;

let net_value = statement.net_value(&converter, &quotes, portfolio.currency()?)?;
let net_value = statement.net_value(&converter, &quotes, portfolio.currency()?, true)?;
let mut commission_calc = CommissionCalc::new(
converter.clone(), statement.broker.commission_spec.clone(), net_value)?;

Expand Down Expand Up @@ -255,7 +255,7 @@ pub fn analyse(
}

pub fn simulate_sell(
config: &Config, portfolio_name: &str, positions: Vec<(String, Option<Decimal>)>,
config: &Config, portfolio_name: &str, positions: Option<Vec<(String, Option<Decimal>)>>,
base_currency: Option<&str>,
) -> GenericResult<TelemetryRecordBuilder> {
let portfolio = config.get_portfolio(portfolio_name)?;
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/portfolio_performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ impl <'a> PortfolioPerformanceAnalyser<'a> {
}

fn process_cash_assets(&mut self, statement: &BrokerStatement) -> EmptyResult {
self.current_assets += statement.cash_assets.total_assets_real_time(
self.current_assets += statement.assets.cash.total_assets_real_time(
self.currency, self.converter)?;
Ok(())
}
Expand Down
25 changes: 17 additions & 8 deletions src/analysis/sell_simulation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use itertools::Itertools;
use static_table_derive::StaticTable;

use crate::broker_statement::{BrokerStatement, StockSell, StockSellType};
Expand All @@ -18,22 +19,30 @@ use crate::util;
pub fn simulate_sell(
country: &Country, portfolio: &PortfolioConfig, mut statement: BrokerStatement,
converter: CurrencyConverterRc, quotes: &Quotes,
mut positions: Vec<(String, Option<Decimal>)>, base_currency: Option<&str>,
positions: Option<Vec<(String, Option<Decimal>)>>, base_currency: Option<&str>,
) -> EmptyResult {
if positions.is_empty() {
positions = statement.open_positions.keys()
let all_positions = positions.is_none();
let positions = positions.unwrap_or_else(|| {
statement.open_positions.keys()
.map(|symbol| (symbol.to_owned(), None))
.collect();
positions.sort();
} else {
for (symbol, _) in &positions {
.sorted_unstable()
.collect()
});

for (symbol, _quantity) in &positions {
if !all_positions {
if statement.open_positions.get(symbol).is_none() {
return Err!("The portfolio has no open {:?} positions", symbol);
}
}
quotes.batch(statement.get_quote_query(symbol))?;
}

let net_value = statement.net_value(&converter, quotes, portfolio.currency()?)?;
let net_value = statement.net_value(
&converter, quotes, portfolio.currency()?,
all_positions // To be able to simulate sell for portfolio with symbols for which quotes aren't available
)?;

let mut commission_calc = CommissionCalc::new(
converter.clone(), statement.broker.commission_spec.clone(), net_value)?;

Expand Down
2 changes: 1 addition & 1 deletion src/bin/investments/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub enum Action {
},
SimulateSell {
name: String,
positions: Vec<(String, Option<Decimal>)>,
positions: Option<Vec<(String, Option<Decimal>)>>,
base_currency: Option<String>,
},

Expand Down
4 changes: 2 additions & 2 deletions src/bin/investments/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,13 @@ impl Parser {
match command {
"buy" => Action::Buy {
name, cash_assets,
positions: self.bought.parse(matches)?.into_iter().map(|(symbol, shares)| {
positions: self.bought.parse(matches)?.unwrap().into_iter().map(|(symbol, shares)| {
(symbol, shares.unwrap())
}).collect(),
},
"sell" => Action::Sell {
name, cash_assets,
positions: self.sold.parse(matches)?,
positions: self.sold.parse(matches)?.unwrap(),
},
"cash" => Action::SetCashAssets(name, cash_assets),
_ => unreachable!(),
Expand Down
10 changes: 5 additions & 5 deletions src/bin/investments/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ impl PositionsParser {
.required(self.required)
}

pub fn parse(&self, matches: &ArgMatches) -> GenericResult<Vec<(String, Option<Decimal>)>> {
let mut positions = Vec::new();

pub fn parse(&self, matches: &ArgMatches) -> GenericResult<Option<Vec<(String, Option<Decimal>)>>> {
let mut args = match matches.values_of(PositionsParser::ARG_NAME) {
Some(args) => args,
None => return Ok(positions),
None => return Ok(None),
};

let mut positions = Vec::new();

while let Some(quantity) = args.next() {
let quantity = if self.allow_all && quantity == "all" {
None
Expand All @@ -58,6 +58,6 @@ impl PositionsParser {
positions.push((symbol.to_owned(), quantity));
}

Ok(positions)
Ok(Some(positions))
}
}
3 changes: 2 additions & 1 deletion src/broker_statement/bcs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ mod tests {
&Default::default(), &Default::default(), &Default::default(), TaxRemapping::new(),
corporate_actions, ReadingStrictness::all()).unwrap();

assert!(!statement.cash_assets.is_empty());
assert!(!statement.assets.cash.is_empty());
assert!(statement.assets.other.is_none()); // TODO(konishchev): Get it from statements
assert!(!statement.deposits_and_withdrawals.is_empty());

assert!(!statement.fees.is_empty());
Expand Down
3 changes: 2 additions & 1 deletion src/broker_statement/firstrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ mod tests {
&Default::default(), &Default::default(), &Default::default(), TaxRemapping::new(),
&[], ReadingStrictness::all()).unwrap();

assert!(!statement.cash_assets.is_empty());
assert!(!statement.assets.cash.is_empty());
assert!(statement.assets.other.is_none()); // TODO(konishchev): Get it from statements
assert!(!statement.deposits_and_withdrawals.is_empty());

assert!(!statement.fees.is_empty());
Expand Down
6 changes: 4 additions & 2 deletions src/broker_statement/ib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ mod tests {
fn parse_real_empty() {
let statement = parse_full("empty", None);

assert!(!statement.assets.cash.is_empty());
assert!(statement.assets.other.is_some());
assert!(statement.deposits_and_withdrawals.is_empty());
assert!(!statement.cash_assets.is_empty());

assert!(statement.fees.is_empty());
assert!(statement.idle_cash_interest.is_empty());
Expand All @@ -279,7 +280,8 @@ mod tests {
let statement = parse_full("my", Some(tax_remapping));
let current_year = statement.period.next_date().year();

assert!(!statement.cash_assets.is_empty());
assert!(!statement.assets.cash.is_empty());
assert!(statement.assets.other.is_some());
assert!(!statement.deposits_and_withdrawals.is_empty());

assert!(!statement.fees.is_empty());
Expand Down
42 changes: 25 additions & 17 deletions src/broker_statement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub struct BrokerStatement {
pub broker: BrokerInfo,
pub period: Period,

pub cash_assets: MultiCurrencyCashAccount,
pub assets: NetAssets,
pub historical_assets: BTreeMap<Date, NetAssets>,

pub fees: Vec<Fee>,
Expand Down Expand Up @@ -198,7 +198,7 @@ impl BrokerStatement {
Ok(BrokerStatement {
broker, period,

cash_assets: MultiCurrencyCashAccount::new(),
assets: NetAssets::default(),
historical_assets: BTreeMap::new(),

fees: Vec::new(),
Expand Down Expand Up @@ -271,18 +271,26 @@ impl BrokerStatement {
QuoteQuery::Stock(symbol.to_owned(), exchanges.get_prioritized())
}

pub fn net_value(&self, converter: &CurrencyConverter, quotes: &Quotes, currency: &str) -> GenericResult<Cash> {
self.batch_quotes(quotes)?;

let mut net_value = self.cash_assets.total_assets_real_time(currency, converter)?;

for (symbol, quantity) in &self.open_positions {
let price = quotes.get(self.get_quote_query(symbol))?;
let price = converter.real_time_convert_to(price, currency)?;
net_value += quantity * price;
pub fn net_value(
&self, converter: &CurrencyConverter, quotes: &Quotes, currency: &str, realtime: bool,
) -> GenericResult<Cash> {
let mut net_value = self.assets.cash.clone();

match self.assets.other {
Some(other) if !realtime => {
net_value.deposit(other);
},
_ => {
self.batch_quotes(quotes)?;

for (symbol, &quantity) in &self.open_positions {
let price = quotes.get(self.get_quote_query(symbol))?;
net_value.deposit(price * quantity);
}
},
}

Ok(Cash::new(currency, net_value))
Ok(Cash::new(currency, net_value.total_assets_real_time(currency, converter)?))
}

pub fn emulate_sell(
Expand Down Expand Up @@ -328,8 +336,8 @@ impl BrokerStatement {
return Err!("The portfolio has no open {} position", symbol);
}

self.cash_assets.deposit(volume);
self.cash_assets.withdraw(commission);
self.assets.cash.deposit(volume);
self.assets.cash.withdraw(commission);
self.stock_sells.push(stock_sell);

Ok(())
Expand All @@ -340,7 +348,7 @@ impl BrokerStatement {

for commissions in commission_calc.calculate()?.values() {
for commission in commissions.iter() {
self.cash_assets.withdraw(commission);
self.assets.cash.withdraw(commission);
total.deposit(commission);
}
}
Expand Down Expand Up @@ -434,9 +442,8 @@ impl BrokerStatement {
}

if let partial::NetAssets{cash: Some(cash), other} = statement.assets {
self.cash_assets = cash.clone();

let assets = NetAssets{cash, other};
self.assets = assets.clone();
assert!(self.historical_assets.insert(self.period.last_date(), assets).is_none());
} else if last {
return Err!("Unable to find any information about current cash assets");
Expand Down Expand Up @@ -643,6 +650,7 @@ impl BrokerStatement {
}
}

#[derive(Clone, Default)]
pub struct NetAssets {
pub cash: MultiCurrencyCashAccount,
pub other: Option<Cash>, // Supported only for some brokers
Expand Down
3 changes: 2 additions & 1 deletion src/broker_statement/open/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ mod tests {
let (namespace, name) = name.split_once('/').unwrap();
let statement = parse(namespace, name);

assert_eq!(statement.cash_assets.is_empty(), name == "inactive-with-forex");
assert_eq!(statement.assets.cash.is_empty(), name == "inactive-with-forex");
assert!(statement.assets.other.is_none()); // TODO(konishchev): Get it from statements
assert!(!statement.deposits_and_withdrawals.is_empty());

assert_eq!(statement.fees.is_empty(), name == "iia");
Expand Down
3 changes: 2 additions & 1 deletion src/broker_statement/tinkoff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ mod tests {
fn parse_real() {
let statement = parse("main", "my");

assert!(!statement.cash_assets.is_empty());
assert!(!statement.assets.cash.is_empty());
assert!(statement.assets.other.is_none()); // TODO(konishchev): Get it from statements
assert!(!statement.deposits_and_withdrawals.is_empty());

assert!(!statement.fees.is_empty());
Expand Down
2 changes: 1 addition & 1 deletion src/portfolio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn sync(config: &Config, portfolio_name: &str) -> GenericResult<TelemetryRec
ReadingStrictness::empty())?;
statement.check_date();

let assets = Assets::new(statement.cash_assets, statement.open_positions);
let assets = Assets::new(statement.assets.cash, statement.open_positions);
assets.validate(portfolio)?;
assets.save(database, &portfolio.name)?;

Expand Down

0 comments on commit f3a0ff1

Please sign in to comment.