Skip to content

Support multiple portfolios and cash positions#2

Merged
igoropaniuk merged 3 commits intomainfrom
feat/multiple_portfolio_cash
Mar 13, 2026
Merged

Support multiple portfolios and cash positions#2
igoropaniuk merged 3 commits intomainfrom
feat/multiple_portfolio_cash

Conversation

@igoropaniuk
Copy link
Owner

No description provided.

Cash holdings can now be tracked alongside stock positions. Each cash
position is stored by currency and included in the USD total using live
forex rates.

New CLI commands:
$ stonks add-cash EUR 5000
$ stonks remove-cash EUR 2000

YAML format:
  portfolio:
    positions:
      - symbol: AAPL
        quantity: 10
        avg_cost: 150.00
        currency: USD
    cash:
      - currency: USD
        amount: 5000.00
      - currency: EUR
        amount: 3000.00

Signed-off-by: Igor Opaniuk <igor.opaniuk@gmail.com>
The total value row was hardcoded to USD. Now each portfolio file can
declare its own base currency so the dashboard reflects the user's
preferred reporting currency.

Example:
  portfolio:
    base_currency: EUR
    positions:
      - symbol: ASML.AS
        quantity: 5
        avg_cost: 680.00
        currency: EUR
    cash:
      - currency: EUR
        amount: 3000.00

Defaults to USD when the field is absent (backwards-compatible).

Signed-off-by: Igor Opaniuk <igor.opaniuk@gmail.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the stonks_cli application by introducing robust support for managing multiple investment portfolios and tracking cash positions. Users can now define and view several portfolios concurrently, each with its own stock holdings and cash balances across different currencies. This change provides a more comprehensive and flexible financial tracking experience, moving beyond a single, stock-only portfolio.

Highlights

  • Multiple Portfolio Support: The application now supports loading and displaying data from multiple portfolio files simultaneously, presenting them in separate tables within the dashboard.
  • Cash Position Management: Introduced the ability to track cash holdings in various currencies within a portfolio, including commands to add and remove cash.
  • Enhanced Portfolio Model: The Portfolio model was extended to include cash positions, a base currency, and a name, improving its flexibility and realism.
  • CLI Improvements: New CLI commands add-cash, remove-cash, and list were added, along with improved portfolio file resolution.
Changelog
  • src/stonks_cli/app.py
    • Imported VerticalScroll and Label from textual.
    • Changed TITLE from "Portfolio" to "Stonks".
    • Updated CSS to use .total class and added .portfolio-header styling.
    • Modified __init__ to accept a list of portfolios and forex_rates as a nested dictionary.
    • Refactored compose to conditionally render a single DataTable or multiple tables within a VerticalScroll based on the number of portfolios.
    • Updated on_mount to add columns to all relevant DataTable instances.
    • Introduced new helper methods _populate_tables, _populate_single, _populate_for, and _render_rows to handle rendering logic for single or multiple portfolios and their positions.
    • Extended _render_rows to display cash positions in the table.
    • Renamed _update_total to _update_total_widget and updated its logic to calculate total market value including cash, considering the portfolio's base currency and forex rates.
    • Modified _refresh_prices to fetch symbols and currencies from all active portfolios and to fetch forex rates for each portfolio's base currency.
    • Updated _apply_prices to use the new _populate_tables method.
  • src/stonks_cli/main.py
    • Imported Portfolio, PORTFOLIO_CONFIG_DIR.
    • Added _resolve_portfolio_path helper to interpret portfolio names or paths.
    • Added _merge_portfolios helper to combine multiple portfolios, handling duplicate symbols and cash.
    • Modified the main Click group to accept multiple --portfolio options and store PortfolioStore instances in ctx.obj["stores"].
    • Updated the dashboard command to load multiple portfolios and pass them to PortfolioApp.
    • Added new Click commands: add-cash to add a specified amount of cash in a given currency to the portfolio, remove-cash to remove a specified amount of cash from a given currency in the portfolio, with validation, and list to list all available portfolio YAML files in the config directory.
  • src/stonks_cli/models.py
    • Introduced CashPosition dataclass with currency and amount fields, including validation and currency normalization.
    • Updated Portfolio dataclass: Added cash: list[CashPosition] field, added base_currency: str field (default "USD"), added name: str field, modified __post_init__ to normalize base_currency and validate for duplicate cash currencies, and added get_cash, add_cash, and remove_cash methods for managing cash positions.
  • src/stonks_cli/storage.py
    • Imported CashPosition.
    • Defined PORTFOLIO_CONFIG_DIR.
    • Updated load method to parse cash, base_currency, and name from the YAML file.
    • Updated save method to persist name, base_currency, and cash positions to the YAML file.
  • tests/test_app.py
    • Updated USD_RATES fixture to match the new nested dictionary structure for forex_rates.
    • Modified all PortfolioApp instantiations to pass portfolios=[portfolio] instead of portfolio=portfolio.
    • Added test_multiple_portfolios_separate_tables to verify rendering of multiple portfolios.
  • tests/test_cli.py
    • Imported new functions and classes: _merge_portfolios, _resolve_portfolio_path, CashPosition, Portfolio, Position, PORTFOLIO_CONFIG_DIR.
    • Updated test_dashboard_passes_portfolio_to_app to check kwargs["portfolios"][0].positions.
    • Added assertion for len(kwargs["portfolios"]) == 1 in test_dashboard_starts_with_empty_prices.
    • Added test_dashboard_shows_with_only_cash.
    • Added TestAddCash class with tests for adding new cash, currency normalization, and accumulation.
    • Added TestRemoveCash class with tests for removing full/partial cash, and error handling for missing currency or excess amount.
    • Added TestResolvePortfolioPath class with tests for portfolio path resolution logic.
    • Added TestList class with tests for listing YAML files, handling no portfolios, missing config directory, and non-YAML files.
    • Added TestMergePortfolios class with tests for merging distinct/duplicate positions, cash positions, and base currency handling.
    • Added TestDashboardMultiplePortfolios class with a test for dashboard displaying separate portfolios.
  • tests/test_models.py
    • Imported CashPosition.
    • Added TestCashPosition class with tests for valid creation, currency uppercase, and error handling for empty/negative/zero amounts.
    • Added tests for Portfolio default base_currency, base_currency normalization, default name, and name preservation.
    • Added tests for Portfolio cash management methods (add_cash, get_cash, remove_cash) and duplicate cash currency validation.
  • tests/test_storage.py
    • Imported CashPosition.
    • Added TestCashPersistence class with tests for saving/loading cash positions, handling missing cash sections, and round-trip mutations.
    • Added TestBaseCurrency class with tests for default base currency, saving/loading, and round-trip normalization.
    • Added TestName class with tests for default name, saving/loading, and round-trip for empty names.
Activity
  • No specific activity (comments, reviews, progress) was provided for this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new functionality by adding support for multiple portfolios and cash positions. The changes are extensive, touching the data models, storage layer, CLI commands, and the TUI application. The implementation is generally of high quality, with good separation of concerns and comprehensive test coverage for the new features.

I've identified a few areas for improvement in the CLI handling of multiple portfolios to enhance usability and prevent potential user errors. Specifically, I've commented on some dead code, a redundant check, and ambiguous behavior in commands that should only operate on a single portfolio. Overall, this is a great enhancement to the application.

@click.pass_context
def add_cash(ctx: click.Context, currency: str, amount: float) -> None:
"""Add AMOUNT of CURRENCY cash to the portfolio."""
store: PortfolioStore = ctx.obj["store"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The add-cash command (and also remove-cash, add, remove) implicitly operates on the first portfolio specified with the -p option when multiple are provided. This can be confusing and lead to unintended modifications. To prevent this, you should add a check to ensure only one portfolio is specified for these commands. I'd also recommend updating the docstring to clarify this behavior.

Suggested change
store: PortfolioStore = ctx.obj["store"]
stores: list[PortfolioStore] = ctx.obj["stores"]
if len(stores) > 1:
raise click.UsageError(
"The 'add-cash' command supports only one portfolio at a time."
)
store: PortfolioStore = ctx.obj["store"]

Each portfolio can be now displayed in its own labelled section
so positions remain clearly separated.

Portfolios also gain a name field in YAML so the section header shows a
meaningful label instead of a generic "Portfolio N" fallback:

portfolio:
  name: Work
  base_currency: USD
  positions: ...

The app title is updated from "Portfolio" to "Stonks".

Multiple portfolios can be passed in several ways.

Full paths:
$ stonks -p ~/finance/work.yaml -p ~/finance/personal.yaml dashboard
Shorthand names (resolves to ~/.config/stonks/<name>.yaml):
$ stonks -p work -p personal dashboard
Mixed:
$ stonks -p work -p /mnt/shared/spouse.yaml dashboard

Omitting -p entirely falls back to ~/.config/stonks/portfolio.yaml.

Signed-off-by: Igor Opaniuk <igor.opaniuk@gmail.com>
@igoropaniuk igoropaniuk force-pushed the feat/multiple_portfolio_cash branch from 5cbb7bc to cb803d1 Compare March 13, 2026 09:16
@igoropaniuk
Copy link
Owner Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds significant new functionality by introducing support for multiple portfolios and cash positions. The changes are well-structured across the model, storage, CLI, and application layers. The introduction of new CLI commands for cash management and portfolio listing is a great addition. The test coverage for the new features is comprehensive. I've identified a minor performance improvement opportunity and a potential point of confusion in the CLI's behavior when handling multiple portfolios with modification commands. Overall, this is a solid contribution.

else:
stores = [PortfolioStore(path=_resolve_portfolio_path(p)) for p in portfolio]
ctx.obj["stores"] = stores
ctx.obj["store"] = stores[0]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Storing only the first portfolio store in ctx.obj["store"] makes commands like add, remove, add-cash, and remove-cash implicitly operate on the first portfolio provided with -p. This can be confusing and lead to unintended modifications when a user specifies multiple portfolios. Consider making these commands either explicitly target a portfolio (e.g., with an additional option) or fail when multiple portfolios are provided, to avoid ambiguity.

Comment on lines +205 to +214
all_symbols = list(
{p.symbol for portfolio in self.portfolios for p in portfolio.positions}
)
extended = fetcher.fetch_extended_prices(all_symbols)
new_prices = {sym: price for sym, (price, _) in extended.items()}
new_sessions = {sym: sess for sym, (_, sess) in extended.items()}
currencies = list({p.currency for p in self.portfolio.positions})
new_forex = fetcher.fetch_forex_rates(currencies)
all_currencies = list(
{p.currency for portfolio in self.portfolios for p in portfolio.positions}
| {c.currency for portfolio in self.portfolios for c in portfolio.cash}
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve efficiency, you can avoid iterating through all portfolio positions twice (once for symbols, once for currencies). You can collect all positions in a single pass, then extract symbols and currencies from that list.

Suggested change
all_symbols = list(
{p.symbol for portfolio in self.portfolios for p in portfolio.positions}
)
extended = fetcher.fetch_extended_prices(all_symbols)
new_prices = {sym: price for sym, (price, _) in extended.items()}
new_sessions = {sym: sess for sym, (_, sess) in extended.items()}
currencies = list({p.currency for p in self.portfolio.positions})
new_forex = fetcher.fetch_forex_rates(currencies)
all_currencies = list(
{p.currency for portfolio in self.portfolios for p in portfolio.positions}
| {c.currency for portfolio in self.portfolios for c in portfolio.cash}
)
all_positions = [
p for portfolio in self.portfolios for p in portfolio.positions
]
all_symbols = list({p.symbol for p in all_positions})
extended = fetcher.fetch_extended_prices(all_symbols)
new_prices = {sym: price for sym, (price, _) in extended.items()}
new_sessions = {sym: sess for sym, (_, sess) in extended.items()}
all_currencies = list(
{p.currency for p in all_positions}
| {c.currency for portfolio in self.portfolios for c in portfolio.cash}
)

@igoropaniuk igoropaniuk merged commit 9c48d57 into main Mar 13, 2026
16 checks passed
@igoropaniuk igoropaniuk deleted the feat/multiple_portfolio_cash branch March 13, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant