Repository navigation
PortfolioDB 1.7.2
The second pass over the 1.7.x audit: six findings the re-audit of 1.7.1
turned up, each a case the first pass had reasoned about and got wrong at
one edge — a split dated after the observation, a dividend paid before a
later split, a liquidated stretch on the chart, an unnamed account beside a
named one, a half-filled CSV trade row, and the write password sitting in the
one container built not to have it. No schema change and no migration.
Upgrading
- If you ran
add_income.py --backfillon 1.7.0 or 1.7.1 for a symbol that
split after one of its dividends, the estimates for the earlier dividends
are undercounted. Rerun with--backfill --replace-estimatesfor that
symbol; it deletes thesource='yfinance'rows first and says how many it
replaced. Manual income rows are never touched. - Compose deployments that run the MCP server on
PORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1must now addPORTFOLIODB_PASSWORD
to themcpservice indocker-compose.override.yml
(exposure shows the block). Deployments
on the read-only role, which is the default, need nothing. - A CSV export with half-filled trade rows that imported clean before will
now be rejected with the line numbers; fix the rows or blank all three
trade fields to make them quote-only.
Security
- The MCP container no longer receives the application's read-write
password. Themcpcompose service inherited the shared environment
block, and with itPORTFOLIODB_PASSWORD, because the pool read the
database address throughload_config(), which insists on that password —
so the one container built to hold only aSELECT-only role also held the
login that could write. The address now comes from a credential-free
db.load_target(), the compose file givesmcpthe address and its own
settings only, and the read-only path never looks at the write password.
The deliberatePORTFOLIODB_MCP_ALLOW_RW_FALLBACK=1opt-out still works but
you now supplyPORTFOLIODB_PASSWORDto the service yourself in
docker-compose.override.yml(exposure
shows the block); without it the server refuses to start and says so.
Fixed
-
CSV import rejects incomplete trade rows instead of skipping them. A
row with some ofTrade Date,Purchase PriceandQuantityfilled in
was treated as "not a lot": nothing was imported for the trade, its price
snapshot still went in, and the run reported zero rejections — a trade
missing from the ledger behind a clean import. Such a row is now rejected
with its line number and the missing field, and so is a trade row with no
symbol; a row with all three trade fields blank is still a quote-only row,
and a wholly blank line is still skipped. The transaction policy applies:
atomic mode writes nothing from that file,--continue-on-errorcounts the
row as rejected and writes the rest. -
The weekly report no longer crashes when an unnamed account meets a named
one. 1.7.0 moved its position aggregation into Python and sorted
(account, symbol)keys with the default ordering, which refuses to compare
Nonewith a string; any ledger with both an account-less lot and a named
account stopped the report before it printed. Unnamed accounts now sort
first, and the stored value is kept as is, soNoneand an empty-string
account stay distinct. -
The portfolio-value chart keeps the days on which everything was sold.
The history dropped every zero-valued point, so a liquidated stretch
vanished from the chart and the line bridged from the last funded day to
the re-entry. Only the points before anything was ever held are dropped
now; a zero after that is drawn as a flat line at zero, and a range that
begins on such a day shows no percentage change rather than a division by
zero. -
Dividend backfill: a dividend paid before a later split is no longer
undercounted. yfinance states every historical per-share dividend in
today's split-adjusted units (Apple's $0.82 of August 2020 comes back as
$0.205 after the 4:1), so the shares it is multiplied by must be in today's
units too. 1.7.0 read the ledger as of the ex-date, which left a later split
out and recorded half (or a quarter) of the cash for every dividend paid
before one. The backfill now selects lots by ex-date and counts them in
current units (ledger_inputs.load(units="current")). Existing
estimates: rows written by earlier backfills carry the old amounts, and
because the dedupe key includes the amount a rerun would insert the
corrected row beside the old one.add_income.py --backfill --replace-estimatesdeletes the symbol'ssource='yfinance'rows first
and reports how many it replaced; manual rows are never touched. -
Historical MCP positions and the daily/EOD report state splits in the
right units.get_positionswith anas_ofbefore a recorded split
applied the split anyway, so "the day before a 2:1" reported twenty shares
against the pre-split quote — twice the value that existed; a stale quote
observed before an ex-date the cutoff was past was joined to restated
shares the same way. The daily/EOD report restated its lots but compared
raw previous, day-start and current quotes, so a split between two quotes
printed a loss of the whole ratio (Delta: $-1,000.00on a pure 2:1 with
nothing else moving). Actions now apply only when dated on or before the
observation date (ledger_inputs.prepare(as_of=…), shared by every
date-filtered reader), stale quotes are restated into the cutoff's units,
and the report's three comparison quotes go through the prepared ledger
with their own timestamps. Installs without acorporate_actionsrow see
no change.