- PHP 100%
| lib | ||
| .gitignore | ||
| _footer.php | ||
| account.php | ||
| admin.php | ||
| annual-report-pdf.php | ||
| auth.php | ||
| backfill.php | ||
| cgt-detail.php | ||
| cgt-pdf.php | ||
| changelog.php | ||
| chart.php | ||
| cost-base.php | ||
| db.php | ||
| edit.php | ||
| favicon.svg | ||
| help.php | ||
| income.php | ||
| index.php | ||
| login.php | ||
| nonce.php | ||
| performance.php | ||
| README.md | ||
| schema.sql | ||
| snapshot.php | ||
| sold.php | ||
| tax-summary-pdf.php | ||
| tax-summary.php | ||
| tax-upload.php | ||
| tax.php | ||
| totp.php | ||
| transactions.php | ||
| what-if.php | ||
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) andview(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_reductionandacb_increasefields indistribution_tax_componentsupdateparcels.cost_base_per_unitaccordingly. - 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.