Skip to content

Commit cd50639

Browse files
committed
Use date for market currency conversion
1 parent 0d439af commit cd50639

File tree

5 files changed

+73
-8
lines changed

5 files changed

+73
-8
lines changed

src/fava_portfolio_returns/api/investments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
def group_stats(p: FilteredPortfolio, start_date: datetime.date, end_date: datetime.date):
2020
balance = p.balance_at(end_date)
21-
cost_value = cost_value_of_inv(p.pricer, p.target_currency, balance)
21+
cost_value = cost_value_of_inv(p.pricer, p.target_currency, balance, end_date)
2222
market_value = market_value_of_inv(p.pricer, p.target_currency, balance, end_date, record=True)
2323
# reduce to units (i.e. removing cost attribute) after calculating market value, because convert.get_value() only works with positions held at cost
2424
# this will convert for example CORP -> USD (cost currency) -> EUR (target currency), instead of CORP -> EUR.

src/fava_portfolio_returns/api/portfolio.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def portfolio_values(
6161
if posting.meta["category"] is Cat.ASSET and posting.cost:
6262
# ex. (CORP, USD)
6363
currency_pairs.add((posting.units.currency, posting.cost.currency))
64+
# also add data points for indirect conversion to target currency
65+
if posting.cost.currency != p.target_currency:
66+
currency_pairs.add((posting.cost.currency, p.target_currency))
6467

6568
def first(x):
6669
return x[0]
@@ -112,8 +115,8 @@ def first(x):
112115
# Clamp start_date in case we cut off data at the beginning.
113116
clamp_date = max(date, start_date)
114117
market = market_value_of_inv(p.pricer, p.target_currency, balance, clamp_date)
115-
cost = cost_value_of_inv(p.pricer, p.target_currency, balance)
116-
cash = -inv_to_currency(p.pricer, p.target_currency, cf_balance) # sum of cash flows
118+
cost = cost_value_of_inv(p.pricer, p.target_currency, balance, clamp_date)
119+
cash = -inv_to_currency(p.pricer, p.target_currency, cf_balance, clamp_date) # sum of cash flows
117120
values.append(PortfolioValue(date=clamp_date, market=market, cost=cost, cash=cash))
118121

119122
return values
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import datetime
2+
import unittest
3+
4+
from fava_portfolio_returns.returns.simple import SimpleReturns
5+
from fava_portfolio_returns.test.test import BEANGROW_CONFIG_CORP
6+
from fava_portfolio_returns.test.test import load_portfolio_str
7+
8+
from fava_portfolio_returns.test.test import approx2
9+
10+
class CurrenciesTest(unittest.TestCase):
11+
def test_indirect_currency_conversion(self):
12+
p = load_portfolio_str(
13+
"""
14+
plugin "beancount.plugins.auto_accounts"
15+
plugin "beancount.plugins.implicit_prices"
16+
17+
2020-01-01 commodity CORP
18+
name: "Example Stock"
19+
20+
2020-01-01 commodity CURRENCY_BASE
21+
2020-01-01 commodity CURRENCY_TARGET
22+
23+
2020-01-01 price CURRENCY_BASE 2 CURRENCY_TARGET
24+
25+
2020-01-01 * "Buy 100 CORP @ 2 CURRENCY_BASE"
26+
Assets:Cash -100.00 CURRENCY_BASE
27+
Assets:CORP 50 CORP {2 CURRENCY_BASE}
28+
29+
2020-01-04 price CURRENCY_BASE 3 CURRENCY_TARGET
30+
31+
2020-02-01 price CORP 3 CURRENCY_BASE
32+
2020-03-03 price CURRENCY_BASE 5 CURRENCY_TARGET
33+
""",
34+
BEANGROW_CONFIG_CORP,
35+
)
36+
p.target_currency = 'CURRENCY_TARGET'
37+
38+
# Cost basis of 100 CURRENCY_BASE = 200 CURRENCY_TARGET
39+
assert p.cash_at(datetime.date(2020, 1, 1)) == 200
40+
# Cost basis of 100 CURRENCY_BASE = 300 CURRENCY_TARGET
41+
assert p.cash_at(datetime.date(2020, 1, 5)) == 300
42+
# Cost basis of 100 CURRENCY_BASE = 300 CURRENCY_TARGET
43+
assert p.cash_at(datetime.date(2020, 2, 1)) == 300
44+
# Cost basis of 100 CURRENCY_BASE = 500 CURRENCY_TARGET
45+
assert p.cash_at(datetime.date(2020, 3, 3)) == 500
46+
47+
returns = SimpleReturns().series(p, datetime.date(2020, 1, 1), datetime.date(2020, 4, 1))
48+
assert returns == [
49+
# 200 CURRENCY_TARGET = 100 CURRENCY_BASE invested
50+
(datetime.date(2020, 1, 1), 0.0),
51+
# currency rate changing doesn't change the cost basis
52+
# (300 CURRENCY_TARGET invested)
53+
(datetime.date(2020, 1, 4), 0.0),
54+
# investment has acrually grown
55+
# (300 CURRENCY_TARGET invested, 50 CORP valued at 150 CURRENCY_BASE = 450 CURRENCY_TARGET)
56+
(datetime.date(2020, 2, 1), 0.5),
57+
# currency rate changing again doesn't change the cost basis (500 CURRENCY_TARGET invested)
58+
(datetime.date(2020, 3, 3), 0.5),
59+
]

src/fava_portfolio_returns/core/portfolio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def cash_at(self, date: datetime.date) -> Decimal:
176176
if cash_flow.date > date:
177177
break
178178
balance.add_amount(cash_flow.amount)
179-
return -inv_to_currency(self.pricer, self.target_currency, balance)
179+
return -inv_to_currency(self.pricer, self.target_currency, balance, date)
180180

181181

182182
def get_target_currency(account_data_list: list[AccountData]) -> str:

src/fava_portfolio_returns/core/utils.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def __init__(self, source: str, target: str, date: Optional[datetime.date]):
2020
)
2121

2222

23-
def cost_value_of_inv(pricer: Pricer, target_currency: str, balance: Inventory) -> Decimal:
23+
def cost_value_of_inv(pricer: Pricer, target_currency: str, balance: Inventory, date: datetime.date) -> Decimal:
2424
cost_balance = balance.reduce(convert.get_cost)
25-
return inv_to_currency(pricer, target_currency, cost_balance)
25+
return inv_to_currency(pricer, target_currency, cost_balance, date)
2626

2727

2828
def market_value_of_inv(
@@ -36,7 +36,7 @@ def market_value_of_inv(
3636
value_balance = balance.reduce(convert.get_value, pricer.price_map, date)
3737

3838
# then convert to target currency
39-
return inv_to_currency(pricer, target_currency, value_balance)
39+
return inv_to_currency(pricer, target_currency, value_balance, date)
4040

4141

4242
def inv_to_currency(
@@ -71,4 +71,7 @@ def get_prices(pricer: Pricer, pair: tuple[str, str]) -> list[tuple[datetime.dat
7171
"""
7272
:param tuple pair: (currency, target_currency), e.g. (CORP, USD)
7373
"""
74-
return prices.get_all_prices(pricer.price_map, pair)
74+
try:
75+
return prices.get_all_prices(pricer.price_map, pair)
76+
except:
77+
raise ValueError(f"Missing prices for currency pair {pair}")

0 commit comments

Comments
 (0)