PHP/MariaDB ASX portfolio tracker and Australian tax app
Find a file
2026-06-09 13:28:36 +10:00
lib Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
.gitignore gitignore: stop tracking CLAUDE.md — Claude Code project instructions, not for public repo 2026-06-04 14:47:35 +10:00
_footer.php v26.06.09-1 — Edit: public holiday awareness for stale data check 2026-06-09 13:28:36 +10:00
account.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
admin.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
annual-report-pdf.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
auth.php Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
backfill.php Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
cgt-detail.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
cgt-pdf.php cgt-pdf.php, tax-summary-pdf.php: restructure PDF header/footer — page 1 header suppressed, page number moved to footer centre, generated-by moved to footer right 2026-06-03 20:34:01 +10:00
changelog.php v26.06.09-1 — Edit: public holiday awareness for stale data check 2026-06-09 13:28:36 +10:00
chart.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
cost-base.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
db.php v26.06.08-1 — DB: TLS connections to MariaDB 2026-06-08 13:32:59 +10:00
edit.php edit: detect ASX public holidays when checking for stale price data 2026-06-09 13:26:34 +10:00
favicon.svg Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
help.php help: expand Users, Backfill Price History, Record CGT Disposal Event descriptions 2026-06-06 07:58:05 +10:00
income.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
index.php index: Cost Basis — extend buy range bar when price is outside buy range 2026-06-06 07:10:34 +10:00
login.php admin.php, login.php, performance.php: show info bubbles on hover instead of click 2026-06-02 17:18:30 +10:00
nonce.php Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
performance.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
README.md README: remove personal deployment URL 2026-06-08 13:34:45 +10:00
schema.sql v26.06.04-1 — Tax Upload: Vanguard AMIT support, units held AJAX, PDF text toggle, remove notes field 2026-06-04 14:42:39 +10:00
snapshot.php Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
sold.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
tax-summary-pdf.php cgt-pdf.php, tax-summary-pdf.php: restructure PDF header/footer — page 1 header suppressed, page number moved to footer centre, generated-by moved to footer right 2026-06-03 20:34:01 +10:00
tax-summary.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
tax-upload.php v26.06.04-1 — Tax Upload: Vanguard AMIT support, units held AJAX, PDF text toggle, remove notes field 2026-06-04 14:42:39 +10:00
tax.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
totp.php Initial commit — Holdings portfolio tracker 2026-06-01 17:18:28 +10:00
transactions.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00
what-if.php v26.06.03-1 — Income, What-If, Annual PDF, Charts, Help pages; AMIT inline on Tax 2026-06-03 22:10:21 +10:00

Holdings

A self-hosted PHP/MariaDB web app for tracking an Australian Share Exchange (ASX) investment portfolio, calculating rebalancing allocations, monitoring performance, and producing Australian tax reports including CGT workings and AMIT adjustments.


Features

Portfolio & Allocation

  • Live prices fetched in parallel from Yahoo Finance at page load
  • Target allocation percentages per ticker stored in the database
  • Rebalancing calculator: enter a new investment amount and the app allocates it across underweight positions to bring the portfolio back to target weights
  • Current value, units held, and allocation drift displayed per position

Performance

  • Portfolio value history charted from daily snapshots
  • Time-Weighted Return (TWR) calculation that strips out capital flows — new money added or withdrawn doesn't inflate/deflate the return figure
  • ASX 200 benchmark overlay (^AXJO via Yahoo Finance, 6-hour cache) — toggle to compare portfolio TWR against the index for any selected period
  • Outperformance/underperformance (alpha) displayed in chart tooltip
  • Selectable date ranges

Tax Reporting

  • CGT Workings — per-parcel capital gains breakdown for a selected financial year, exportable as a branded PDF
  • Tax Summary — aggregated income and capital gains report for a selected FY, exportable as PDF
  • AMIT upload — upload Betashares AMIT statement PDFs; the app parses tax components (labels 13U, 13C, 13Q, 13R, 13A, 18A, 18H, 20E, 20M, 20O) and adjusts parcel cost bases accordingly
  • Supports FIFO, minimum-CGT, and maximum-CGT disposal methods per sell event
  • CGT 50% discount automatically applied for parcels held longer than 365 days
  • Franking credits, foreign income tax offsets, and DRP units tracked

Transactions

  • View all buy, sell, and distribution reinvestment transactions
  • Upload Betashares CSV transaction exports
  • Manual edit of transaction records
  • Sold securities view showing realised history

Security

  • Username + bcrypt password authentication
  • Optional TOTP two-factor authentication (RFC 6238, SHA1/6-digit/30s) — no external dependencies, pure PHP implementation
  • Brute-force lockout: 3 failed attempts (password or TOTP) triggers a per-username cooldown
  • IP-level blocking via admin panel
  • Role-based access: full (read/write) and view (read-only)
  • Sessions expire after 1 hour of inactivity
  • Strict CSP, HSTS, X-Frame-Options, and other security headers set by nginx
  • Database connections use TLS (TLSv1.3) with server certificate verification

Admin

  • User management: create, lock, unlock, reset passwords
  • TOTP management: enable/disable per user
  • IP block management

Tech Stack

Component Detail
Language PHP 8.5
Database MariaDB
Web server Nginx
PDF generation FPDF (bundled in lib/)
Price data Yahoo Finance v8 API (no key required)
Authentication Custom — bcrypt + RFC 6238 TOTP

No Composer dependencies for the main app. FPDF is bundled directly.


Australian Tax Notes

  • Financial year: 1 July 30 June, labelled by the end year. FY26 = 1 Jul 2025 30 Jun 2026.
  • Cost base method: average cost (as required for managed funds / ETFs under Australian tax law).
  • Realised gains: not stored in the database. Recomputed on demand from the full transaction history.
  • AMIT (Attribution Managed Investment Trust): annual tax statements adjust parcel cost bases up or down. The acb_reduction and acb_increase fields in distribution_tax_components update parcels.cost_base_per_unit accordingly.
  • CGT discount: 50% discount applied to net capital gains where the parcel was held for more than 365 days (individual taxpayer rules).

Database Schema

parcels                    One buy lot per row. cost_base_per_unit adjusted by AMIT.
cgt_events                 One row per sell transaction.
parcel_disposals           Parcel-level breakdown of each CGT event (FIFO / min / max CGT).
distribution_tax_components AMIT attribution per distribution payment per ticker.
transactions               Betashares transaction export rows (raw import).
portfolio_snapshots        Daily portfolio totals (value, cost, gain/loss).
holding_prices             Daily per-ticker price and value.
holdings_meta              Target allocation % and display metadata per ticker.
users                      Accounts with bcrypt password, role, TOTP secret.
login_attempts             Per-username attempt log for brute-force lockout.
blocked_ips                IP-level access block, managed via admin panel.

Full DDL is in schema.sql.


Project Structure

index.php            Portfolio overview and rebalancing calculator
performance.php      Portfolio value chart, TWR, ASX 200 benchmark
tax.php              Tax report selector (CGT workings, tax summary)
cgt-detail.php       Per-parcel CGT workings for a selected FY
cgt-pdf.php          CGT workings PDF export
tax-summary.php      Aggregated tax summary for a selected FY
tax-summary-pdf.php  Tax summary PDF export
tax-upload.php       AMIT PDF upload and parsing
transactions.php     Transaction list
edit.php             Transaction editor
sold.php             Sold securities view
snapshot.php         Daily price snapshot cron script
backfill.php         Backfill historical price data
account.php          User account (password, TOTP)
admin.php            Admin panel (users, IPs)
login.php            Authentication
auth.php             Auth helpers
db.php               Database connection
totp.php             Pure-PHP TOTP implementation (RFC 6238)
nonce.php            CSRF nonce helpers
_footer.php          Shared footer with version and licence
changelog.php        Release changelog
config.php           Database credentials (not committed — see setup)
schema.sql           Full database schema
nginx/home           Nginx vhost config
lib/                 FPDF library
cache/               Runtime cache (axjo.json — not committed)

Setup

1. Database

Create a MariaDB database and import the schema:

mysql -u USER -p DB_NAME < schema.sql

2. Configuration

Copy or create config.php with your database credentials:

<?php
return [
    'host'     => 'your-db-host',
    'dbname'   => 'your-db-name',
    'username' => 'your-db-user',
    'password' => 'your-db-password',
    'charset'  => 'utf8mb4',
    'ssl'      => true,   // set false if your DB server does not support SSL
];

config.php is excluded from version control via .gitignore.

3. Cache directory

Create the cache directory with correct ownership:

mkdir -p /var/www/html/holdings/cache
chown www-data:www-data /var/www/html/holdings/cache

4. Nginx

Deploy the vhost config and reload:

scp nginx/home root@your-server:/etc/nginx/sites-available/home
ssh root@your-server "nginx -t && systemctl reload nginx"

5. First run

Visit /holdings/login.php — on first load with no users in the database, a setup form prompts you to create the initial admin account.

6. Snapshot cron

Add a cron job to record daily portfolio snapshots (runs after market close):

30 16 * * 1-5 /usr/bin/php /var/www/html/holdings/snapshot.php >> /var/log/holdings-snapshot.log 2>&1

Deployment

After editing a file locally, SCP it to the server:

scp FILE.php root@your-server:/var/www/html/holdings/FILE.php

Licence

© David Klaverstyn. Licensed under CC BY-NC-SA 4.0.