Foundational knowledge
What is an ETF?
An ETF is a regulated fund that holds a portfolio (stocks, bonds, commodities, crypto, etc.) and issues shares listed on exchanges. Corgi is the issuer; investors hold fund shares, not direct ownership of every underlying stock.
Two markets — primary vs secondary
ETF activity splits into two markets. Secondary is investors buying and selling existing ETF shares on exchanges — this is the vast majority of volume. Primary is Authorized Participants (APs) creating or redeeming shares directly with the fund — this changes shares outstanding and AUM, but happens far less often.
US average · ICI
Europe approx. · ~2:1 sec:prim
Architecture — today vs target
Five fixed roles — only the company inside each box changes. Fund accounting always pulls market data and operational truth into a fund administrator, which produces NAV and GL for the issuer.
Roles
| Role | Responsibility | Accountability |
|---|---|---|
| Issuer / sponsor | ETF issuer; consumes NAV, GL, and regulatory outputs | Owns the fund; does not run daily accounting |
| Fund administrator | NAV engine, GL, trial balance, fee accruals, corp-action application, EOD batch | Reconciles all inputs; signs off NAV before publish |
| Custodian | Safekeeping of fund assets; holdings, cash, settlement, trade confirms | Warrants accuracy of custody records under custody agreement |
| Market data vendor | EOD prices, FX, corporate actions, security reference, venue turnover | Vendor SLA on data quality; admin reconciles before use |
| Transfer agent | Share registry; creations / redemptions processing; shares outstanding | Share-count accuracy; feeds operational truth into admin |
What changes: fund administrator and transfer agent move from U.S. Bank GFS to Corgi in-house. Custodian and market data vendors stay on existing contracts. Bloomberg / ICE feed the admin's pricing pipelines — not the custodian. Custodian data is a separate input (dataset 4) governed by the custody SLA, not the admin outsourcing agreement.
Data sourcing
Fund accounting requires six input datasets — five core plus product-specific add-ons. See architecture for how market data vendors, custodian, and administrator roles map to these inputs. Market data (datasets 1–3, 5–6) typically comes from one vendor contract (Bloomberg below); operational data (dataset 4) originates at the custodian and is consumed by the fund administrator. Each pipeline lists one primary, one backup, and one cross-check vendor.
| # | Dataset | What it provides | Primary | Backup | Cross-check |
|---|---|---|---|---|---|
| 1 | Pricing & FX | Equity EOD marks, fixed income clean/dirty prices, FX spot rates, price quality flags | Bloomberg | ICE | LSEG |
| 2 | Corporate actions & FI events | Dividends, splits, mergers, bond calls, maturities, coupon changes, partial redemptions | Bloomberg | ICE | LSEG |
| 3 | Security reference | ISIN/CUSIP/SEDOL, bond coupon/maturity/day-count, asset class, withholding rates | Bloomberg SECF | ICE Ref Data | LSEG |
| 4 | Operational truth | Custodian holdings & cash, trade history, shares outstanding, AP create/redeem confirms | U.S. Bank Custody | FundGuard | In-house ledger |
| 5 | Venue turnover | Per-ISIN secondary-market turnover by listing venue (LSE, Xetra, Euronext…) | Bloomberg ECP | LSEG | Desk feed |
| 6 | Crypto spot | Spot bid/ask/last for direct crypto holdings — only if fund holds crypto | Kraken | Coinbase | — |
Other product-specific add-ons: derivatives marks (2×/buffer funds), index/benchmark levels (buffer funds). Internal fund master and fee schedules are configuration — enriched on top of dataset 3, not a separate vendor pipeline.
Procurement paths
Redundancy rules (market-data pipelines)
| Pipeline | Reconciliation rule |
|---|---|
| Equity price | Block NAV if primary vs backup differ by more than tolerance (bps) |
| FI price | Evaluated marks — wider tolerance; flag illiquid names |
| Corp actions | Do not apply until 2-of-3 vendors agree on ex-date and rate |
| FX | Exact match expected on WM/Reuters fix |
Datapoints
Every line on the fund general ledger and trial balance maps to a datapoint below. Reference GL: EUV thematic equity fund (211161). Bond funds (e.g. 211265) add fixed-income lines. Groups reflect Corgi product type — not GL sub-category codes.
Click any row to expand the description and formula. One source of truth — no separate detail section.
Field prefixes: D1 pricing & FX · D2 corp actions & FI events · D3 security reference · D4 operational truth · CFG fund config (fee schedule, base currency)
Method: Pulled = maps 1:1 to a single normalized input field · Computed = combines two or more inputs via an explicit formula
| Group | Datapoint | Method |
|---|---|---|
| All ETFs | Cash | Computed |
|
Settled and unsettled cash balances across all currencies, converted to fund base currency for the trial balance. D4.cash_settled + D4.cash_unsettled per currency × D1.fx_rate → CFG.base_currency
|
||
| All ETFs | Investment receivable | Computed |
|
Amounts due from sales not yet settled. Bridges trade date and settlement date. Σ D4.trade_proceeds where side = sell AND settlement_date > as_of
|
||
| All ETFs | Investment payable | Computed |
|
Amounts owed on purchases not yet settled. Bridges trade date and settlement date. Σ D4.trade_cost where side = buy AND settlement_date > as_of
|
||
| All ETFs | Dividend withholding tax payable | Computed |
|
Withholding tax accrued on dividends, not yet remitted. Σ (D2.dividend_gross × D3.withholding_rate) per holding on D2.ex_date; unpaid balance
|
||
| All ETFs | Accrued unitary fee expense | Computed |
|
Daily accrual of unitary management fee based on fund net assets. CFG.fee_bps × net_assets ÷ days_in_period
net_assets = total_assets − total_liabilities |
||
| All ETFs | Settlement breakage income | Computed |
|
Difference between expected and actual cash on AP create/redeem settlement. D4.ap_settlement_expected − D4.ap_settlement_actual
|
||
| All ETFs | Withholding tax — dividends (expense) | Computed |
|
Withholding tax expense recognized on dividend entitlement (P&L). Σ (D2.dividend_gross × D3.withholding_rate) recognized on D2.ex_date
|
||
| All ETFs | Unitary fee expense | Computed |
|
Periodic recognition or payment of accrued unitary fee. accrued_unitary_fee balance recognized or paid in period
(from CFG.fee_bps × net_assets accrual) |
||
| All ETFs | Accounts receivable — subscriptions | Pulled |
|
Cash owed to the fund from AP creations not yet settled. D4.ap_create.pending_cash_due
|
||
| All ETFs | Accounts payable — redemptions | Pulled |
|
Cash owed on pending AP redemptions not yet settled. D4.ap_redeem.pending_cash_owed
|
||
| All ETFs | Subscriptions / redemptions (capital) | Pulled |
|
Settled primary-market capital flow for the period. D4.ap_flow.capital_settled (creates − redeems)
|
||
| All ETFs | Total assets | Computed |
|
Sum of all asset general ledger lines. Σ all asset GL lines (cash + investments + receivables + accrued income)
|
||
| All ETFs | Total liabilities | Computed |
|
Sum of all liability general ledger lines. Σ all liability GL lines (payables + accrued fees + tax payable)
|
||
| All ETFs | Net assets | Computed |
|
Fund net asset value before per-share division. total_assets − total_liabilities
|
||
| All ETFs | Shares outstanding | Pulled |
| All ETFs | NAV per share | Computed |
| All ETFs | Net income | Computed |
|
P&L for the period. Σ income accounts − Σ expense accounts for period
|
||
| All ETFs | Total capital | Computed |
|
Shareholder equity roll-forward. subscriptions_capital + accumulated_unrealized_GL + retained_earnings
|
||
| Equity ETF | Cost of investments — equity | Computed |
|
Original purchase price of equity holdings, per tax lot, adjusted for corporate actions. Σ D4.tax_lot.cost_basis adjusted for D2.corp_action (splits, mergers, spin-offs)
|
||
| Equity ETF | Net unrealized appreciation/depreciation — equity | Computed |
|
Mark-to-market gain or loss on equity holdings vs cost basis. Σ (D4.holdings_qty × D1.eod_price − cost_basis) per equity position
|
||
| Equity ETF | Dividends receivable | Computed |
|
Dividends declared but not yet paid. Σ D2.dividend_declared_unpaid per holding where D2.pay_date > as_of
|
||
| Equity ETF | Dividend income | Computed |
|
Dividend income recognized on ex-date. Σ D2.dividend_gross recognized on D2.ex_date for D4.holdings_qty on record date
|
||
| Equity ETF | Accumulated net unrealized G/L (capital) | Computed |
|
Capital-side roll-forward of unrealized appreciation/depreciation. prior_accumulated_unrealized + Δ net_unrealized_equity for period
|
||
| Equity ETF | Realized gain/loss — short term | Computed |
|
Profit or loss on equity sales with holding period under one year. Σ (D4.trade_sale_proceeds − matched D4.tax_lot.cost_basis) where holding_period < 1 yr
|
||
| Bond ETF | Cost of investments — fixed income | Computed |
|
Amortized cost basis of bond holdings per tax lot. Σ D4.tax_lot.cost_basis adjusted for D2.fi_events; amortized cost per D3.day_count
|
||
| Bond ETF | Net unrealized appreciation/depreciation — FI | Computed |
|
Mark-to-market gain or loss on bonds vs amortized cost. Σ (D4.holdings_qty × D1.fi_clean_price − amortized_cost) per bond
|
||
| Bond ETF | Investment interest receivable | Computed |
|
Accrued coupon not yet received. Σ accrued coupon where pay_date > as_of
D3.coupon_rate × D4.holdings_par ÷ D3.day_count_basis |
||
| Bond ETF | Investment interest income | Computed |
|
Coupon income accrued for the period. Σ daily coupon accrual
D3.coupon_rate × D4.holdings_par ÷ D3.day_count_basis |
||
| Bond ETF | Amortization of premium | Computed |
| Bond ETF | Accretion of discount | Computed |
|
Daily accretion of bond purchase discount to par. (D3.par − D4.tax_lot.purchase_price) × accretion_factor
factor from D1.yield_to_maturity + D3.day_count |
||
| Bond ETF | Realized gain/loss — short term | Computed |
|
Profit or loss on bond sales with holding period under one year. Σ (D4.trade_sale_proceeds − matched amortized_cost) where holding_period < 1 yr
|
||
| Treasury ETF | Cost of investments — short-term | Computed |
|
Cost basis of treasury bill and note holdings. Σ D4.tax_lot.cost_basis adjusted for D2.fi_events (bills, notes)
|
||
| Treasury ETF | Investment interest receivable | Computed |
|
Accrued coupon on treasury holdings not yet received. Σ accrued coupon where pay_date > as_of
D3.coupon_rate × D4.holdings_par ÷ D3.day_count_basis |
||
| Treasury ETF | Investment interest income | Computed |
|
Coupon income on treasury holdings for the period. Σ daily coupon accrual
D3.coupon_rate × D4.holdings_par ÷ D3.day_count_basis |
||
| Treasury ETF | Realized gain/loss — short term | Computed |
|
Profit or loss on treasury sales with holding period under one year. Σ (D4.trade_sale_proceeds − matched D4.tax_lot.cost_basis) where holding_period < 1 yr
|
||
Internal fields
Not everything comes from a vendor feed or a formula. Corgi must store and maintain fund-specific configuration and identifiers — the fund master. Without these, datapoint computes have nothing to key on.
| Field | Why we store it | Example |
|---|---|---|
| Fund ticker | Maps vendor security IDs to Corgi product; used in reports and baskets | DNRO, EUV |
| Fund legal name | Regulatory filings, investor communications | Corgi Global X … ETF |
| Share class | Multi-class funds; fee and NAV per class | Class A / institutional |
| Fund share ISIN / CUSIP | ETF share identifier — distinct from underlying holdings | US… / XS… |
| Base currency | FX conversion anchor for trial balance | USD, EUR |
| Fee schedule (bps) | Unitary fee accrual — no vendor supplies this | 20 bps on net assets |
| Fiscal year-end | Period close, annual audit boundary | Dec 31 |
| Authorized participants | Create/redeem settlement and breakage rules | AP bank list |
| Listing venues | Turnover report venue columns | LSE, Xetra, NYSE Arca |
| GL chart mapping | Datapoint → GL account code | 211161 EUV mapping |
| Recon tolerances | Validation thresholds (bps, absolute) | Equity price ±5 bps |
| NAV cutoff time | EOD batch schedule per market | 4:00 PM ET equity close |
| Settlement calendar | T+1 US / T+2 EU rules per market | US equity T+1; EU T+2 |
| Distribution groups | Email recipients per report type | daily_volume_report → desk + issuer |
Stored in CFG.* referenced throughout datapoint formulas. Changes are versioned and effective-dated — not overwritten silently.
Data pipelines
Receiving data is the easy part — a vendor CSV drop over SFTP/FTP. The hard part is when files arrive, what you store, how you validate before normalization, and how you prove it later. Everything below assumes we persist data; we do not compute NAV from live API calls at publish time.
Why end-of-day?
Fund accounting runs on an as-of date, not in real time. A trade executed today does not settle today — US equities are T+1 (next business day); EU markets are typically T+2. Until settlement, the position is in investment receivable/payable, not cash. Market data vendors publish official EOD marks only after the close. Running the batch intraday would mean marking on incomplete prices and booking unsettled trades as settled.
EOD batch logic: wait for (1) market close + official prices, (2) custodian day-end file, (3) corp-action confirmations — then compute, reconcile, sign off, publish.
Ingest & compute (AWS)
Feeds arrive as CSV exports pushed by vendors (and U.S. Bank GFS during calibration) over SFTP or FTP. AWS pattern: Transfer Family exposes the endpoint → files land in an S3 landing prefix → Lambda fires on ObjectCreated.
Lambda does two things on every file:
- Archive — move CSV to immutable S3 hierarchy:
archive/{vendor}/{feed}/{as_of_date}/{received_at}_{filename}. This is the raw layer — we do not need a separate JSONB extract table when the vendor artifact is already a file. - Normalize — parse validated rows into typed
core.*tables. Idempotent per(as_of, feed, row_key)so reprocessing a file replaces the same logical rows, never silently duplicates.
Downstream EOD (ledger, reports, email) chains via EventBridge once required feeds for an as_of are validated + loaded. CloudFront Functions are edge HTTP only; batch work runs on Lambda in-region.
core.*). Carl Clements owns ledger and fund accounting principles — datapoint formulas, GL logic, NAV compute from normalized inputs onward. The handoff is a clean boundary: normalized tables in, ledger out.
CSV validation — before normalized insert
Skipping validation and jumping straight to core.* is the main ingest failure mode. If our parser is wrong, we only find out when NAV breaks — too late. Validate before insert; on failure, quarantine the file, log to reporting.ingest_exceptions, alert ops, and block ledger for that feed/as_of.
| Failure | Typical cause | Gate |
|---|---|---|
| Schema drift | New, renamed, or missing column; column reorder | Strict column contract per feed (versioned). Unknown column → block. Missing required column → block. |
| Format change | Date pattern flip, decimal separator, quoted-field breakage, encoding shift | Typed parsers with explicit formats; reject rows that fail cast. |
| Bad / empty data | Nulls in required fields, negative qty where impossible, duplicate keys | Row-level rules + aggregate sanity (row count vs prior day, sum checks). |
| Wrong file | Bad as_of in filename, duplicate upload, truncated/partial file | Filename pattern match; file-size floor; checksum; dedupe on (feed, as_of, checksum). |
| Late or missing file | SFTP push delayed, vendor outage | EventBridge cutover timer; missing required feed → block EOD, alert. |
| Parser bug (ours) | Mapping typo, wrong delimiter assumption | Immutable S3 archive lets us replay; dev calibration diffs catch systematic errors. |
Each feed ships with a CSV contract in repo config: expected columns, types, required/nullable, filename pattern, and schema version. Vendor changes the file → contract version bumps → parser update → replay from archive. Never mutate a archived CSV.
Cross-day sanity tests
After schema validation, compare today's file against T−1 loaded data. These are cheap catches for stale or broken drops — run on every feed before core.* insert.
| Test | What it catches | Typical response |
|---|---|---|
| Identical to yesterday | Vendor re-sent stale file; as_of wrong inside file | Block · quarantine · alert (your example — check all key metrics) |
| Row count collapse | Truncated export, empty file, parser read wrong sheet | Block if count < floor or >50% drop vs T−1 without explanation |
| Row count spike | Duplicate rows, wrong fund rolled in, delimiter break | Block if >2× T−1 without corp-action / trade event flag |
| All prices unchanged | Stale marks on an equity pricing file | Block pricing feed when 100% of liquid names match T−1 |
| Holdings qty unchanged | Stale custodian positions after known trading day | Warn or block when trades exist in confirms but holdings file is flat |
| Cash / NAV flat | Custodian or TA file not refreshed | Block when aggregate cash, shares outstanding, or fund-level NAV equals T−1 on active day |
Filename vs content as_of | Wrong file in right folder | Block on mismatch between filename date and max date in rows |
| Checksum duplicate | Same bytes uploaded twice | Skip second file; log as duplicate, do not re-load |
| Outlier move | Single bad price or qty typo | Flag rows where Δ vs T−1 exceeds CFG tolerance (e.g. price >20%, qty >50%) |
Thresholds live in CFG.recon_tolerances per feed — not hardcoded. Log every failed test to reporting.ingest_exceptions with T and T−1 aggregates for ops review.
SFTP re-pull on stale or failed file
If cross-day or schema checks fail — file identical to yesterday, wrong as_of, truncated drop, etc. — do not load into core.*. Instead:
- Quarantine the file and log the failure reason
- Trigger an SFTP re-pull for that feed (Lambda polls vendor outbox or requests re-push per vendor SLA)
- Re-run validation on the new file; cap retries (e.g. 3 attempts before escalating)
- If still bad at EOD cutover → block ledger for that
as_ofand alert ops
Re-pull only applies when the CSV itself is suspect — not when our parser is wrong. Parser bugs are fixed in code and replayed from the S3 archive.
Ledger output checks (before GL insert)
Second failure point: bad calculations, not bad CSV. EventBridge invokes ledger Lambda after core.* is loaded. Before writing to ledger.*, sanity-check computed values against CFG.recon_tolerances — NAV delta vs T−1, GL line bounds, impossible negatives. Values outside the realm of possibility → block insert, log exception, do not publish.
Index and market-data providers (e.g. MSCI, Bloomberg) set the pattern fund admins mirror:
- Scheduled file delivery — daily packages over SFTP/FTP at published cutover windows; ASCII/delimited modules per dataset (index level, security level, constituents — we split D1–D5 the same way)
- Versioned file contracts — vendors announce schema changes before they ship; we version contracts and never silently accept unknown columns (MSCI explicitly reserves the right to change formats with advance notice)
- Immutable archive — every delivery retained as-received; replays and audits trace to the original file, not a re-export
- Validate → load — no direct insert into downstream tables; quarantine on failure; idempotent load per
as_of - Distribution cycles — EventBridge cutover only after required modules for the date are present and validated (mirrors MSCI's fixed daily delivery schedule)
- API optional, files canonical — providers offer APIs for flexibility; batch CSV/SFTP remains the audit trail fund accounting relies on
Our ingest methodology should be indistinguishable from how a Bloomberg or MSCI client would wire a fund admin — modular feeds, strict contracts, archived raw files, gated normalize.
Ingest metadata
Lightweight ingest.files table — not a second copy of the data:
Storage model
Four layers — raw is the CSV file on S3, not a parsed duplicate in Postgres.
| Layer | Where | Contents | Owner |
|---|---|---|---|
| Raw | s3://archive/ + ingest.files | Immutable CSV as received; metadata row per file (checksum, status, schema version) | Engineering |
| Normalized | core.* | Typed columns: price_eod, holdings_qty, corp_action… Parsed only after CSV validation passes | Engineering |
| Ledger | ledger.* | Datapoint values, GL lines, NAV — produced from core.* + CFG.* | Carl Clements |
| Recon & reporting | reporting.* | Ingest exceptions, calibration diffs (dev), vendor scorecard (prod). Does not block writes but gates EOD sign-off | Shared |
| Report archive | s3://reports/ | Rendered HTML (daily volume report, future packs) | Engineering |
Encryption: TLS in transit; AES-256 at rest (cloud KMS or on-prem HSM). Fund accounting data is not PCI, but it is fiduciary — treat as confidential. On-prem is viable for latency/control; cloud is viable for ops cost. Either works if access is role-scoped and audit-logged.
Validation — development vs production
Two phases. Do not conflate them.
Development (pre-launch calibration)
While building the in-house engine, the trusted reference is U.S. Bank GFS output — labeled ground truth from the system we are replacing. Each EOD:
- Run our best-effort datapoint functions on the same inputs
- Import U.S. Bank trial balance / NAV extract for the same
as_of - Diff every GL line and datapoint — log to
reporting.calibration_diffs - Where we disagree, backsolve: trace inputs → formula → find the break
- Fix compute, rerun until output is congruent
After several days of this loop, calculations should match on all standard paths. Remaining gaps are edge cases — corp-action quirks, FI amortization corners — where domain review (operations team) closes the last mile before we hit them in production.
Production (post-launch vendor reconciliation)
Once live, U.S. Bank is no longer the reference — three market-data vendors per pipeline are. When primary (A) ≠ backup (B), cross-check (C) breaks the tie.
Every exception writes to reporting.vendor_exceptions: field, as_of, vendor flagged, values from A/B/C, resolution. Over time this becomes a vendor reliability scorecard. Three sources are sufficient for tiebreak — if A, B, and C all disagree, that is a block-NAV event.
Transform
core.* → ledger.* via datapoint formulas (detail cards) — owned by Carl. Transforms are deterministic, versioned, and replayable from archived CSV + normalized rows. No ledger run for an as_of until required feeds show status = loaded in ingest.files.
Retention
SEC Rule 204-2 requires investment advisers to retain books and records for at least five years (first two in primary office). NAV history, GL, reconciliation logs, and vendor CSV archives qualify. Design default: 7 years online on S3, then glacier — covers examination cycles with margin. Immutable CSV archive makes replay and audit defensible without a parallel JSONB store.
Fund administration workflows
Industry-standard daily and periodic processes the system must support — not exotic, but all mandatory for a regulated ETF administrator.
| Workflow | Frequency | Trigger → outcome |
|---|---|---|
| EOD NAV batch | Daily | Feeds arrive → ingest → validate → compute datapoints → reconcile custodian → sign-off → publish official NAV |
| Trade & settlement | Daily | Trade date books receivable/payable; settlement date (T+1/T+2) moves cash and clears bridge accounts |
| Corporate actions | Event-driven | Vendor alert → 2-of-3 confirm → adjust qty/cost/income → post GL entries |
| AP create / redeem | As occurred | TA confirm → basket vs holdings rec → settlement → update shares outstanding and capital |
| Fee accrual | Daily | CFG.fee_bps × net_assets → accrued unitary fee; periodic payment clears accrual |
| Calibration (dev) | Daily until cutover | Corgi compute vs U.S. Bank reference → diff → backsolve → fix until congruent |
| Reconciliation (prod) | Daily | Custodian vs internal; vendor A vs B vs C; all breaks cleared before NAV sign-off |
| Exception resolution | As needed | Pricing override, corp-action confirm, manual JE — audit trail required |
| Month / quarter / year-end close | Periodic | Accrual true-ups, financial statement pack, audit support file |
Major outputs
Established artifacts every fund administrator produces — regulatory, operational, and investor-facing. These are not optional nice-to-haves; they are what auditors, the SEC, APs, and the issuer expect.
| Output | Audience | Frequency | Source |
|---|---|---|---|
| Official NAV / NAV per share | TA, exchanges, issuer, APs | Daily | Computed ledger · signed off |
| Trial balance / general ledger | Internal, audit, issuer | Daily | All datapoint GL lines |
| Portfolio valuation | Issuer, compliance | Daily | Holdings × marks + accruals |
| Schedule of investments | SEC (N-PORT), annual report | Quarterly / annual | Holdings snapshot |
| Creation / redemption basket file | Authorized participants | Daily + on demand | Holdings + cash component for CU |
| Daily volume / flow report | Corgi issuer, distribution | Daily | Dataset 5 + Dataset 4 · see Daily report |
| Reconciliation & exception report | Operations, vendor management | Daily | reporting.vendor_exceptions |
| SEC regulatory filings | SEC | Periodic | N-CEN, N-PORT, financial statements |
| Audit support package | External auditor | Annual | GL, NAV history, S3 CSV archive replay, rec sign-offs |
| Investor factsheet / performance | Investors, marketing | Monthly | NAV series, AUM, benchmark |
Daily fund report
Corgi runs a Daily Volume Report every business day — all funds on one HTML page. Tracks AUM, primary-market flow (creates / redeems), secondary-market turnover by venue, and NAV deltas. This is an issuer-facing operations report, distinct from the official NAV publish.
Report structure
Matches the provided sample (daily_volume_report_anonymised_.html). Skeleton generator: daily_volume_report.py.
| Panel | Contents |
|---|---|
| Daily AUM change | Per-fund flow (create/redeem), flow vs price decomposition, BOD/EOD AUM |
| YTD AUM change | Gross creates, gross redeems, flow + price attribution, YoY AUM |
| Per-fund detail | Ticker, passive/active, inception, name, TER, theme, AUM, flow (1D/T+1/30D/YTD), venue turnover columns (LSE, Xetra, Euronext…), NAV per share + delta % |
Flow columns = primary market (AP creates/redeems). Turnover columns = secondary market volume by listing venue (Dataset 5).
Generation pipeline (AWS)
| Step | AWS service | Detail |
|---|---|---|
| Trigger | EventBridge | Fires after EOD NAV sign-off (or fixed cutover time) |
| Render | Lambda | Query reporting DB → build HTML from template → all Corgi funds |
| Store | S3 | Immutable archive: reports/daily_volume/{YYYY-MM-DD}.html — full history retained |
| Distribute | SES | HTML body embedded in email to recipients in CFG.distribution_groups |
| Access | CloudFront + S3 | Optional: serve historical reports via CDN for internal dashboard |
Distribution groups stored in fund config — not hardcoded. Example: daily_volume_report → desk, issuer, distribution list. AWS SES identity and suppressions managed in-console; recipient lists live in our DB so ops can update without redeploying Lambda.
Sophisticated report packs (monthly performance, audit extracts) follow the same pattern: Lambda render → S3 archive → SES to a different distribution group. Detail deferred — same infrastructure.
The HTML report is a distribution output (email + archive) — not the main UI. Day-to-day work happens in the web app.
UI
The most critical feature is calculating reliable figures — eliminating dependence on U.S. Bank as fund administrator. A polished UI is secondary but matters for selling the system to other firms later.
Web first — faster iteration, easier demos to prospective clients. The daily volume HTML report is a distribution artifact we generate and send; the web app is where accountants work.
Specific screens and layout TBD — validate by shadowing the current U.S. Bank workflow once we have access. Desktop shell (Electron) only if a buyer requires on-prem deployment.
Implementation plan
- Get access to the current accounting system and backend — learn how it works, view the data
- Stand up SFTP → S3 → Lambda ingest with CSV + cross-day validation; load
core.*tables - Carl owns ledger from normalized data; engineering supports ingest and calibration diffs vs U.S. Bank
- Shadow U.S. Bank UI after access; define web app from discovered workflows — not before
- Once calibrated: EventBridge EOD chain, report render + SES, web app — keep diffing