Skip to content

Commit

Permalink
Throw an error message for invalid tickers + allow decimal values for…
Browse files Browse the repository at this point in the history
… sum to invest (#6)

* Throw an error message for invalid tickers
- Print an error message when the portfolio CSV-file contains tickers that
don't exist on Yahoo finance.
- Allow decimal values to be used for the sum to invest input.

* Bump up version number
Bump up version number in anticipation for version 1.0.1.
  • Loading branch information
EmilMaric authored Apr 10, 2021
1 parent cbcd2f4 commit be87d6b
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 15 deletions.
2 changes: 1 addition & 1 deletion portfolio_rebalancer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def portfolio_rebalancer():
"your portfolio as close as possible to "
"your target allocation given a sum to "
"invest."))
@click.argument('sum-to-invest', type=click.INT)
@click.argument('sum-to-invest', type=click.FLOAT)
@click.option('-p', '--portfolio', 'portfolio_csv', required=True,
help="CSV-file containing the portfolio and target allocations.")
def calc(sum_to_invest, portfolio_csv):
Expand Down
4 changes: 4 additions & 0 deletions portfolio_rebalancer/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ def __init__(self):
def __getitem__(self, asset_name):
return self._assets[asset_name]

@property
def assets(self):
return self._assets

def add_asset(self, asset):
self._total += asset.price * asset.qty
self._assets[asset.name] = asset
Expand Down
13 changes: 8 additions & 5 deletions portfolio_rebalancer/portfolio_csv_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def _sanitize_target_allocation(self, target_allocation, line_num):
def get_portfolio(self):
portfolio = Portfolio()
total_allocation_pct = 0.0
seen_assets = set()
with open(self.portfolio_csv, newline='') as f:
reader = csv.reader(f)
for line_num, row in enumerate(reader):
Expand All @@ -52,15 +51,19 @@ def get_portfolio(self):
shares = self._sanitize_shares(row[1], line_num)
target_allocation = self._sanitize_target_allocation(row[2],
line_num)
asset = PortfolioAsset(name, shares, target_allocation)
if name not in seen_assets:
seen_assets.add(name)
try:
asset = PortfolioAsset(name, shares, target_allocation)
except KeyError:
raise ClickException(
"Row {} - ticker {} doesn't exist".format(line_num,
name))
if name not in portfolio.assets:
portfolio.add_asset(asset)
else:
raise ClickException("Asset name {} appears twice on row "
"{}".format(name, line_num))
total_allocation_pct += target_allocation
if total_allocation_pct != 100:
raise ClickException("Total combined allocation percentage of all "
"rows is over 100%.")
"rows is over 100%")
return portfolio
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "portfolio-rebalancer"
version = "1.0.0"
version = "1.0.1"
description = "A CLI tool that shows you what to buy to rebalance your portfolio as best as possible to your target allocations."
license = "MIT"
readme = "README.md"
Expand Down
41 changes: 33 additions & 8 deletions tests/test_portfolio_rebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_multiple_buys(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '100'],
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '100.00'],
catch_exceptions=False)

assert result.exit_code == 0
Expand Down Expand Up @@ -59,7 +59,7 @@ def test_same_drift(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'],
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'],
catch_exceptions=False)

assert result.exit_code == 0
Expand All @@ -85,7 +85,7 @@ def test_malformed_shares_value(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'])
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'])

assert result.exit_code == 1
assert result.output == """\
Expand All @@ -109,7 +109,7 @@ def test_malformed_target_allocation_value(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'])
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'])

assert result.exit_code == 1
assert result.output == """\
Expand All @@ -136,7 +136,7 @@ def test_same_asset_multiple_times(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'])
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'])

assert result.exit_code == 1
assert result.output == """\
Expand All @@ -162,11 +162,11 @@ def test_portfolio_target_allocation_over_100(testfiles_dir, ticker_mock):
data=test_portfolio)

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'])
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'])

assert result.exit_code == 1
assert result.output == """\
Error: Total combined allocation percentage of all rows is over 100%.
Error: Total combined allocation percentage of all rows is over 100%
"""


Expand All @@ -180,10 +180,35 @@ def test_no_assets(testfiles_dir, ticker_mock):
"test_portfolio.csv",
data=test_portfolio)
runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30'],
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'],
catch_exceptions=False)

assert result.exit_code == 1
assert result.output == """\
Error: Row 0 malformed - expecting row in this format: TICKER SHARES_OWNED TARET_ALLOCATION
""" # noqa: E501


def test_keyerror(testfiles_dir, mocker):
"""Test that if the portfolio CSV-file contains a ticker that can't be
found on Yahoo finance.
"""
test_portfolio = [
['MFST', '1', '100']
]
test_portfolio_csv = create_csv_file(testfiles_dir,
"test_portfolio.csv",
data=test_portfolio)

mock_obj = mocker.patch(
'portfolio_rebalancer.portfolio_asset.yf.Ticker.info')
mock_obj.__getitem__.side_effect = KeyError

runner = CliRunner()
result = runner.invoke(pr, ['calc', '-p', test_portfolio_csv, '30.00'],
catch_exceptions=False)

assert result.exit_code == 1
assert result.output == """\
Error: Row 0 - ticker MFST doesn't exist
""" # noqa: E501

0 comments on commit be87d6b

Please sign in to comment.