Share Dinkum is a free, open-source app for tracking investment portfolios. It runs on your own computer and opens in your browser. It handles shares, ETFs and funds on any exchange and in any currency, plus unlisted holdings, and works out the Australian tax on them: franking credits, AMIT cost base adjustments, foreign income and capital gains, including the 2027 changes.
Your data stays yours. It's stored on your own computer, not someone else's server. Export portfolio on the dashboard writes all of it to one Excel workbook with every column clearly explained, so you can read it, load it back in, or move to another tool if you ever need to. Full backup also copies your attached documents. Every feature is free, with no paid tier, and the AGPL licence keeps the code open for anyone to use and improve.
This project is under active development, and contributions and feedback are welcome.
Share Dinkum is not tax advice. Check its figures against your statements, or with a tax agent, before relying on them in a return.
Usage is governed by the license.
See CHANGELOG.md for what has changed in each release.
Enter records in the web interface, or in bulk from Excel using the data_import.ipynb notebook,
which loads one Excel file into one portfolio (see Data import instructions).
To export, create a [DataExport]. An export can be loaded back into the portfolio it came from to
update it.
Each portfolio is an [Account], with a base currency, a fiscal year configuration (Australian by default) and tax settings.
With more than one portfolio, choose which one you are working on from the Portfolio list at the top of every page. The dashboard, its reports and buttons, every admin list, and new records all follow it, and a record can only refer to records of its own portfolio. A portfolio you add in the admin is selected for you. Full backup is the exception: it always covers every portfolio. A superuser (every install that signs in automatically) can select any portfolio; any other user only their own.
A [Market], eg ASX, contains [Instrument] objects, eg BHP or VDHG.
Each purchase is a [Buy]: date, quantity, unit price and brokerage, plus an optional file and notes. A buy in a currency other than the base currency gets an [ExchangeRate] for its date; a day with no quote (a weekend or holiday) takes the last rate before it.
Each [Buy] gives a [Parcel]: a holding of shares with the same purchase date and cost base per share. Parcels are worked out from the trades, so whenever a trade is added, changed or deleted, its instrument's parcels are worked out again in date order. Trades can be entered in any order.
Income is entered as [Dividend] and [Distribution] objects, and classified into a [FiscalYear] according to the account's [FiscalYearType].
A [Sell] is allocated against parcels by a strategy: FIFO (first in, first out), LIFO (last in, first out), MIN_CGT (minimise net capital gain) or MANUAL (you create the allocations, usually only when importing legacy data). The sell creates [SellAllocation] objects linking it to parcels. Each allocation is a capital gain or loss, classified into a [FiscalYear].
When a [SellAllocation] does not consume a whole parcel, the parcel is bifurcated: it is marked inactive and replaced by a 'sold' and an 'unsold' parcel, which share its cost base and point back to it.
An AMIT [CostBaseAdjustment] is apportioned to the unsold parcels, weighted by quantity times days held in the fiscal year, as [CostBaseAdjustmentAllocation] objects. You can also allocate it manually.
A [ShareSplit], dated on its ex-date, takes the units held before and after. It replaces the affected parcels with new ones at the adjusted quantity and cost base, and moves their cost base adjustment allocations across.
Decisions already made are kept when parcels are worked out again: which parcels each sale used, and how each adjustment was divided (see Cost base adjustments). A change those decisions cannot follow, such as reducing a buy below what was sold from it, or a share split dated before a sale already allocated, is refused.
Prices are stored as traded on the day in [InstrumentPriceHistory]. Use Refresh prices on
the dashboard to fetch new prices and exchange rates. If the dashboard reports a data problem, run
uv run dev repair_portfolio_data (--dry-run only reports).
Further tables hold tax facts the reports read, such as residency periods and loss carry-forwards: see Capital gains tax. Every table is in the detailed data model.
flowchart LR
Market --> Account
Instrument --> Market
InstrumentPriceHistory --> Instrument
Buy --> Instrument
Sell --> Instrument
Dividend --> Instrument
Distribution --> Instrument
CostBaseAdjustment --> Instrument
ShareSplit --> Instrument
Parcel --> Buy
Parcel --> Parcel
SellAllocation --> Parcel
SellAllocation --> Sell
CostBaseAdjustmentAllocation --> CostBaseAdjustment
CostBaseAdjustmentAllocation --> Parcel
ShareSplit --> Parcel
Not shown: AppUser, FiscalYearType, FiscalYear, ExchangeRate, CurrentExchangeRate, LogEntry, DataExport, and the capital gains tables below.
For the full entity relationship diagram with fields, see Detailed data model.
Capital gains reports are worked out from your transactions when you run them, so a correction flows through to every report. The account's taxpayer type sets the discount: half for an individual or trust, a third for a complying super fund, none for a company.
The 2027 changes are legislated (Treasury Laws Amendment (Tax Reform No. 1) Act 2026) and apply to CGT events from 1 July 2027. Turn on Model 2027 regime in the account's tax settings to apply them. A holding bought before and sold after 1 July 2027 is split: the discount on growth to 30 June 2027, CPI indexation after.
Eight tables support this: residency periods (discount apportionment, TAP / NTAP), instrument valuations (deemed disposals), managed fund attribution statements and their components, capital loss carry-forwards, CGT return snapshots and their rows, and CPI figures. Instruments also gain a legal form, which drives the CGT schedule report.
Related commands (run with uv run dev): load_cpi, capture_cutover_valuations,
suggest_instrument_classification and capture_cgt_snapshot.
- docs/capital_gains_models.md: what each table is for and the order to fill them in.
- CHANGELOG.md, version 0.4.0: the full list of behaviour.
All screenshots use the fake sample portfolio.
The dashboard has the actions (refresh prices, CGT snapshot and report, income report, export, backup) and charts of the portfolio.
The Australian CGT report sets out each year in the layout of the tax return's CGT schedule.
The Australian income report sets out each year's dividends and trust income by return label: items 11 (dividends), 13 (trusts, from each fund's annual statement) and 20 (foreign income). Only income received as an Australian resident counts. Payments while a foreign resident are listed separately, with what was withheld.
Each AMIT cost base adjustment is spread over the parcels held in its year, weighted by units and days held. A trade entered later, dated before an adjustment already spread, leaves that spread as it was, and the admin says so. Select the adjustments and choose Reallocate to spread them over the holding as it now stands; a confirmation page shows each buy's share before and after, and the sales whose gain would change.
All data is stored in a local database, so you can also connect your own BI tools to it.
These steps are written for Windows 10/11 using PowerShell, and work the same on macOS and Linux except where noted. They take about ten minutes.
You need two tools. You do not need to install Python separately: uv downloads the correct
version (3.13) for you.
| Tool | What it is for | Install with winget |
Or download |
|---|---|---|---|
| Git | Downloads the code | winget install Git.Git |
git-scm.com |
| uv | Manages Python and the dependencies | winget install astral-sh.uv |
docs.astral.sh/uv |
After installing, close and reopen your terminal so it picks up the new commands. Check both work:
git --version
uv --versiongit clone https://github.com/pretoriusdre/share-dinkum.git
cd share-dinkumuv syncThis creates a .venv folder, downloads Python 3.13 if needed, and installs the package versions
pinned in uv.lock. You never need to activate the virtual environment: every command below uses
uv run, which does it for you.
Nothing to do: the first time the app starts it copies share_dinkum_proj/.env.sample to
share_dinkum_proj/.env and fills in a random SECRET_KEY. The defaults use a local SQLite
database. Open .env afterwards to move the backup or media folders (see Updating, below).
From the repository root (cd .. if you are still in share_dinkum_proj):
uv run dev migrateuv run devThen open http://127.0.0.1:8000/ in your browser. There is no login: the app runs on your own
machine against your own database, so it signs you in automatically. If you ever make an install
reachable by anyone else, put LOCAL_AUTO_LOGIN=False in .env and create an account with
uv run dev createsuperuser, and the normal login page comes back.
Leave the terminal open while you use the app. Press Ctrl+C there to stop the server.
| Problem | Fix |
|---|---|
git or uv is not recognised |
Close and reopen the terminal after installing, so PATH updates. |
Activate.ps1 cannot be loaded because running scripts is disabled |
You do not need to activate anything, use the uv run commands above. |
That port is already in use |
Something else is on port 8000. Run uv run dev runserver 8001 and browse to port 8001. |
No such file or directory: manage.py |
uv run dev works from the repository root. Use cd to get back there. |
| Browser shows "DisallowedHost" | Use 127.0.0.1, not your machine name. |
Updating stops with Your local changes to the following files would be overwritten by merge |
You have changed a file that the update also changes, most often data_import.ipynb because running it rewrites its saved output. Copy it elsewhere if you want to keep your version, then git checkout -- share_dinkum_proj/data_import.ipynb and update again. Working in your own data_import_private.ipynb copy avoids this. |
Any other Django command can be passed straight through, so uv run dev test share_dinkum_app runs
the test suite and uv run dev collectstatic collects static files.
The dashboard shows the installed version next to Recent actions. Once a day it checks GitHub for a newer release and, if there is one, says so with a link to what changed. Offline, it says nothing. CHANGELOG.md has the full history, including any one-off steps after an upgrade.
To update, stop the server with Ctrl+C, then from the repository root run:
uv run update.pyThat backs up your data, fetches the latest code, installs any new dependencies, and applies any
database changes. Then start the app again with uv run dev.
The backup goes to ~/share-dinkum-backups/main/ (the five most recent are kept) and covers both
parts of your data: the database share_dinkum_proj/db.sqlite3, and share_dinkum_proj/media, which
holds documents attached to transactions. To roll back, stop the server and copy both back. Neither,
nor your .env, is part of the repository, so an update never changes them.
To keep backups on another drive, add a line to share_dinkum_proj/.env, for example
BACKUP_LOCATION=D:\. The backups then go in D:\share-dinkum-backups. The update, the dashboard and
the notebook all use it. Existing backups are not moved, so move the old share-dinkum-backups folder
there yourself. Every backup includes a copy of media, which holds past exports, so clear old
exports out of it if space is tight.
MEDIA_LOCATION=D:\share-dinkum-data keeps media in D:\share-dinkum-data\media.
Both settings name the folder that should hold the share-dinkum-backups or media folder, not that
folder itself. It can be your Documents folder, your home folder or a whole drive (D: means D:\),
because Share Dinkum only ever writes to, prunes, copies, empties or restores the folder it names
inside it. Stop the server and move the existing media folder there first, since attached
documents are found by their path inside it. If a folder called media is already there with other
files in it, a restore or clear-all leaves those files alone, but every backup copies them, and the
start-up check says so.
If you have edited any of the project's own files, the update stops before changing anything and lists them, so your edits cannot be lost in a merge. Commit or discard them and run it again.
Running the steps individually
uv run update.py is equivalent to backing up your data and then running:
git pull
uv sync
uv run dev migrategit pull brings the new code, uv sync installs added or changed dependencies, and
uv run dev migrate applies database changes. Skipping the last one typically shows up as an error
about a missing column or table.
You can bulk load your share data from Excel.
From the repository root, generate an empty template:
uv run dev make_import_templateThat writes share_dinkum_proj/share_dinkum_app/import_data/data_import_template_blank.xlsx, with
every sheet and column the loader understands. Each column header has a note saying what it is,
whether it is required, and what a blank means; columns with a fixed set of values have a dropdown.
Take one copy per portfolio:
- Windows (PowerShell or Command Prompt):
cd share_dinkum_proj\share_dinkum_app\import_data copy data_import_template_blank.xlsx data_import_template_private.xlsx
- macOS/Linux:
cd share_dinkum_proj/share_dinkum_app/import_data cp data_import_template_blank.xlsx data_import_template_private.xlsx
Anything ending in _private.xlsx is excluded from the repository, so updates leave your files alone.
The sheets with a grey tab (residency periods, instrument valuations, capital loss carry-forwards and
managed fund annual statements) are optional. Leave them empty, or enter that information in the app
later. CPI figures are not part of the file: load them with uv run dev load_cpi.
For worked examples, data_import_template_public.xlsx is the same template filled with a fake
10-year portfolio (uv run dev make_fake_data --force rebuilds it).
Enter your share data in data_import_template_private.xlsx using Excel.
Take your own copy of the notebook, as you did with the template. Your _private copy is excluded
from the repository, so updates leave it alone. Run that one, not the original.
From the repository root:
- Windows (PowerShell or Command Prompt):
copy share_dinkum_proj\data_import.ipynb share_dinkum_proj\data_import_private.ipynb
- macOS/Linux:
cp share_dinkum_proj/data_import.ipynb share_dinkum_proj/data_import_private.ipynb
Open share_dinkum_proj/data_import_private.ipynb and run the cells in order, either:
-
In VS Code (simplest on Windows): install the Python and Jupyter extensions, open the file, and select the
.venvinterpreter when prompted. -
In your browser, without installing Jupyter permanently:
uv run --with notebook jupyter notebook share_dinkum_proj/data_import_private.ipynb
Loading only adds to or updates the portfolio a file is listed against, as a single transaction: a file that fails part way leaves nothing behind. It is safe to run again after a new year of trades. The one cell that deletes anything is at the bottom of the notebook under Danger zone, commented out. It wipes every portfolio, not just one.
Each Excel file loads into exactly one portfolio. List them in the portfolios list near the top of
your data_import_private.ipynb:
portfolios = [
{
'description': 'Default Portfolio',
'input_file': import_data_folder / 'data_import_template_private.xlsx',
'taxpayer_type': 'INDIVIDUAL', # optional: or TRUST, PARTNERSHIP, COMPANY, SMSF
'tax_settings_reviewed': True, # optional: silences the tax settings warning
},
{
'description': "Partner's Portfolio",
'input_file': import_data_folder / 'partner_private.xlsx',
},
]Portfolios are matched by description, so keep them distinct. Adding a portfolio and running again
loads only the new one. taxpayer_type and tax_settings_reviewed apply only when the portfolio is
first created; you can also set them in the app.
- Rows are matched on
id, thenlegacy_id, then the table's unique fields (such as a market's code), and otherwise added. A corrected file therefore updates rows rather than duplicating them. - A blank cell leaves the stored value unchanged.
- A transaction row with no
idorlegacy_idis refused once the portfolio has rows of that kind, since it cannot be matched and would be added twice. - A [DataExport] file can be loaded back into the portfolio it came from, but not into a different one: its rows carry their identity, so loading it elsewhere would move them rather than copy them.
Share Dinkum can read your statements and list where a portfolio differs from them: a payment, trade or statement line it holds differently, or lacks. It only reads; nothing in the portfolio is changed.
cd share_dinkum_proj
uv run dev check_documents --account "Default Portfolio" path/to/documents --out findings.xlsxEvery folder inside the one you give is read too. It reads:
- contract notes from CommSec and SelfWealth
- dividend and distribution advices from Computershare, Link Market Services / MUFG Corporate Markets, Advanced Share Registry, Boardroom and Vanguard, including dividend reinvestment advices
- annual tax statements from Computershare (Vanguard's ETFs) and Link / MUFG (BetaShares and trusts)
- CHESS holding statements, whose balances are checked against the units your trades give
- Interactive Brokers activity statements, saved as CSV
A scanned PDF needs OCR, which runs on your computer: install it with uv sync --inexact --extra ocr.
OCR can misread a figure, so the workbook marks each scanned file. Check a finding against the
document before changing the portfolio.
uv run dev redact_documents path/to/documents --out path/to/copies writes a text copy of each
document with your personal details taken out: the terms you list in
share_dinkum_proj/redact_terms.txt (your name, addresses and phone numbers, one per line), and
every TFN, HIN, SRN, bank account and email address. It prints only counts and line numbers, and
points at any line that still looks personal. Read the copies before sharing them, and share only
the copies.
check_documents --ai reads a layout no parser knows with an AI model, local (Ollama) or hosted,
set up in .env. Every document is redacted before it is sent, and a hosted AI is refused until
you have a terms list.
Each layout is read by one file in share_dinkum_proj/share_dinkum_app/documents/parsers/, with a
made-up example document and the events it should give. uv run dev parser_prompt path/to/copy.txt
writes a brief for a new one from a redacted copy; PARSER_SPEC.md in that folder is the contract
the tests check every parser against. Never put a real document in the repository.
Contributions are welcome. To contribute:
- Fork the repository.
- Create a new feature branch:
git checkout -b feature-name - Make your changes and commit:
git commit -m "Describe your changes" - Push your changes:
git push origin feature-name - Open a pull request on GitHub
This project is licensed under the GNU Affero General Public License (AGPL). You are free to use, modify, and distribute the software under the terms of the AGPL.
Limitations of Liability
This software is provided "as is" without warranty of any kind, either express or implied. The authors are not liable for any claims or damages resulting from its use.
Usage at Your Own Risk
By using this software, you acknowledge that it is your responsibility to ensure it meets your needs. The authors disclaim responsibility for any losses or issues arising from its use.
For full details, see the LICENSE file.








