Helm Portfolio for Dummies
Syllabus Next →
Module 09 of 10

Strengths, Weaknesses & Gotchas

An honest assessment of what Portfolio does well, where it falls short, and the top 10 gotchas to be aware of.

20 minutes Completed Module 8

Strengths: What Portfolio Does Well

Before we get to the hard truths, it's worth understanding where Portfolio genuinely excels. Key strengths worth understanding:

Comprehensive transaction taxonomy
17 transaction types covering the full PE fund + direct investment lifecycle. Subscriptions, capital calls, distributions, valuations, transfers, fees, fund-level transactions, adjustments -- the model handles real-world complexity, not just the happy path.
Robust calculation engine
A dedicated Go microservice handles all performance calculations via 27 gRPC endpoints. XIRR, TVPI, DPI, RVPI, allocation breakdowns, currency conversion -- computed on demand with no stale-cache problems. The calc service is a significant piece of engineering.
Multi-tenant isolation
Schema-per-tenant architecture means complete data segregation at the database level. One tenant's data cannot leak into another's, even in the face of application bugs. This is table stakes for regulated financial software.
Bulk import with reversal
Import large datasets via XLSX, and if something is wrong, reverse the entire import with one API call. The ImportTracker records every created record ID, so reversal is precise -- not a blunt "delete everything created today."
Flexible asset hierarchy
Parent-child relationships between assets, plus sub-positions for share classes and underlying investments. A PE fund can hold direct investments as children, each with its own valuation and transaction history.
Three API surfaces
REST API for the UI, Machine API for system-to-system integration with Delio Core, and MCP API for advanced automation. Different auth mechanisms, different coverage, different use cases -- but all operating on the same data.
Whitelabel reporting
Configurable branding (colors, logos), glossary terms, disclaimers, and layout. Clients can make the reporting UI look like their own product. PDF generation with custom templates.
Investor self-service ("My View")
Investors get their own portal with document access, transaction history, and the ability to submit transaction requests with an approval workflow. Reduces the back-and-forth between GPs and LPs.
Multi-currency with exchange rate conversion
Assets and transactions can be in any currency. The calc service converts everything to the reporting currency using stored exchange rates. Not just display formatting -- actual FX conversion in the calculation pipeline.

Weaknesses: Where Portfolio Falls Short

These are not bugs. They are architectural limitations and missing capabilities that affect what you can realistically promise to clients.

NOT a general ledger
Portfolio covers roughly 15-20% of what a PE fund general ledger needs. No double-entry bookkeeping, no chart of accounts, no allocation engine, no waterfall/carried interest calculations. If a client asks "can it replace our fund accounting system?" the answer is no.
Data ingress bottleneck
XLSX is the only comprehensive bulk path. The Machine API only covers ~7 of 17 transaction types. There are no custodian/bank feeds, no fund admin data ingestion, and no reconciliation workflow. Getting large volumes of data into Portfolio reliably remains the hardest operational challenge.
No GP/LP role distinction at the data model level
Everyone is just an "Investor" in the data model. There's no differentiated GP vs LP views at the data layer. The UI has "My View" for investors, but the underlying model doesn't distinguish between a GP managing the fund and an LP investing in it.
Commitment lifecycle is incomplete
The investor_asset_commitments table was dropped in September 2025 and not replaced with an equivalent. Commitment tracking now relies on subscription records, which don't fully model the commitment-call-payment chain that PE funds use.
Fund structures are too flat
No explicit support for master-feeder structures, parallel funds, or AIVs (Alternative Investment Vehicles). The parent-child asset hierarchy helps, but there's no first-class concept of a fund-of-funds relationship with proper consolidation.
No performance attribution at the model level
The calc service produces aggregate metrics (TVPI, DPI, XIRR), but there's no way to attribute performance to specific factors -- sector, vintage, geography, manager. Clients who need attribution analysis must export and do it externally.
Limited ILPA compliance
Portfolio produces the right metrics (TVPI, DPI, RVPI, XIRR) but not in the standardized ILPA template format. Institutional LPs who require ILPA-compliant reports will need post-processing or manual reformatting.
gRPC service coupling
Calculation results are cached in Redis, so warm caches still serve during a calc service outage. But any cache miss when the Go service is down produces an error — there's no degraded mode, no stale-data fallback, and no indicator to the user that they're seeing cached vs. live numbers. A calc service outage after a cache-clearing data import means a blank dashboard until the service recovers.
No incremental/delta import
Every XLSX import is a full batch process. You can't import "just the changes since last time." For clients doing regular data refreshes, this means re-importing everything or carefully managing which rows are new.
No reconciliation workflow
There's no way to compare imported data against external sources (fund admin reports, custodian statements). No matching, no exception reporting, no reconciliation dashboard. Clients must reconcile manually.
Cash flow forecasting not supported
Portfolio records historical transactions and calculates current metrics, but there's no forward-looking capability. No expected capital call schedules, no distribution forecasts, no liquidity planning tools.
Setting expectations correctly

Portfolio is a portfolio reporting and investor servicing tool, not a fund accounting system. Keeping that boundary clear during client conversations helps set the right expectations.

The Top 10 Gotchas

Non-obvious behaviors and edge cases worth knowing about. Click each card to reveal the details and the fix.

Sheets are processed in the order they appear in the Excel workbook. If a sheet references data created by a later sheet, it fails. This is a common import failure.

The dependency chain: Investors → Assets → Cash Pools → Subscriptions → Capital Call Groups → Capital Calls → Valuations → Distributions → Transfers

Groups must come before Calls. Subscriptions must come before Distributions. Parent assets must come before children.

The fix

Always arrange your workbook tabs in dependency order. If in doubt, follow the chain above. The "Shared" sheet handles sorting automatically, but dedicated sheets do not.

Both "Capital Call Groups" and "Capital Repayment Groups" use the same importer class (InvestorGroupsImport) and write to the same singleton cache. When the second Groups sheet is processed, it completely overwrites the cache -- it doesn't append.

This means if Capital Repayment Groups is processed after Capital Call Groups, the capital call group mappings are gone from memory. If Capital Calls hasn't been processed yet, it will fail because its groups are no longer in the cache.

The fix

Always process in this exact order: Capital Call Groups → Capital Calls → Capital Repayment Groups → Capital Repayments. Never reorder these four sheets.

All exists validation rules include a deleted_at IS NULL condition. If an investor or asset was soft-deleted (still in the DB but marked as deleted), any import row that references it will fail with "The investor field must exist" -- even though the record does exist in the database.

This is especially confusing because you can see the record in the database but the import insists it doesn't exist.

The fix

Before importing, check for soft-deleted records that share client_reference values with your import data. Either restore them (clear deleted_at) or use different client references. The admin UI doesn't show soft-deleted records by default, so you may need to check the database directly.

Import error messages reference row numbers from the heading row, not from the top of the sheet. Most sheets use heading row 2 (row 1 is a title/description). So "Row 7" in an error message means the 7th data row after the heading -- which might be row 9 in Excel.

Some importers override this: DistributionImport and FundTransactionsImport use heading row 2, while ValuationsReplaceImport uses heading row 1. The offset differs per sheet type.

The fix

When debugging an error, always check which row the heading is on for that sheet type. Add the heading row number to the error's row number to get the Excel row. Download the official templates to see where each heading row starts.

The "Valuations" sheet is additive only. If a valuation already exists for the same asset + date + investor combination, the import fails. This catches accidental duplicate imports, but it also means you can't update a valuation that was imported with the wrong amount.

The "Valuations - Replace" sheet will create new valuations or overwrite existing ones for the same date. But it has a different heading row (row 1 instead of row 2), which trips people up when switching between sheets.

The fix

Use "Valuations" for initial imports. Use "Valuations - Replace" for corrections or ongoing updates. When switching, remember to adjust for the different heading row offset. The all - replace investor value on the Valuations sheet also works for replacing.

When you configure an asset, you choose a subscription type: drawdown, units_assigned, or fully_paid_up. This applies to all investors in that asset. You cannot have Alice on a drawdown subscription and Bob on a units-assigned subscription in the same fund.

This surprises people who expect per-investor flexibility, especially for funds that have different share classes with different subscription mechanics.

The fix

If you need different subscription types for different investors, create separate assets (e.g., "Fund I - Class A" and "Fund I - Class B") with a parent-child relationship. Each child asset can have its own subscription type.

These are two different enums that sound like they should be the same thing. A TransactionType describes what happened (SUBSCRIPTION, DISTRIBUTION, CAPITAL_CALL, etc.). A CashFlowType describes how the calc service categorizes the cash movement (DRAWDOWN, REDEMPTION, DIVIDEND, etc.).

The mapping is not 1:1. A SUBSCRIPTION transaction creates DRAWDOWN cash flows. A DISTRIBUTION transaction can create REDEMPTION, DIVIDEND, or RETURN_OF_CAPITAL cash flows depending on the distribution type. People confuse the two when building integrations.

The fix

When working with the calc service or reading calculation results, you're dealing with CashFlowTypes. When creating transactions via import or API, you're dealing with TransactionTypes. Keep a mapping reference handy and don't assume they match by name.

There's an asset-level toggle called overwriteCGWithReturns. When enabled, the "Capital Gains" metric on the dashboard is actually showing Total Returns instead. The label doesn't change -- it still says "Capital Gains" -- but the underlying calculation is different.

Clients may look at the dashboard, see "Capital Gains: $2.5M" and think that's actual capital gains, when it's really total returns including income distributions. This causes confusion in client conversations and reporting discrepancies.

The fix

Document which assets have this toggle enabled. When onboarding clients, explicitly ask whether they want "Capital Gains" to show actual capital gains or total returns. If the toggle is on, make sure everyone involved in reporting knows the label is misleading.

The Machine API (used for Delio Core integration) only supports ~7 of 17 transaction types: investor create/update, subscriptions, investments, capital calls, and asset show. It cannot create distributions, fees, fund cash movements, asset transfers, valuations, or adjustments.

This means you cannot fully automate the data pipeline from Delio Core to Portfolio. Some transaction types will always require manual XLSX import or direct REST API calls with a user session.

The fix

When designing an integration, map out which transaction types you need and check Machine API coverage first. For unsupported types, plan for either XLSX generation + upload via the bulk import API, or use the MCP API which has broader coverage (8 import tools). Don't promise full automation until you've verified coverage.

Before you can import subscriptions, distributions, capital calls, or cash pool transactions for an asset, the INVESTOR_SUBSCRIPTION_DISTRIBUTION toggle must be enabled on that asset. If it's not, every row referencing that asset fails with "The asset must have subscriptions/distributions enabled."

This toggle is separate from asset creation. You can create an asset via import, but if you don't enable this toggle before importing transactions, the transaction import fails. The toggle is not settable via the XLSX import -- it requires a UI action or API call.

The fix

After importing assets, enable the INVESTOR_SUBSCRIPTION_DISTRIBUTION toggle on each asset via the UI (Asset Settings) or the REST API before attempting any transaction imports. Build this step into your onboarding checklist.

Quick Reference: Gotchas at a Glance

#GotchaImpactPrevention
1Sheet orderingEntire sheets fail to importFollow dependency chain
2BulkImportCacheCapital calls lose group mappingsStrict sheet order for groups/calls
3Soft-deleted records"Not found" for existing recordsCheck deleted_at before import
4Heading row offsetWrong row when debugging errorsKnow your sheet's heading row
5Valuations vs ReplaceDuplicate errors on updatesUse Replace sheet for corrections
6Subscription type scopeAll investors get same typeUse child assets for different types
7Transaction vs CashFlow typesWrong enum in integrationsKeep mapping reference
8overwriteCGWithReturnsMisleading dashboard labelsDocument toggle state per asset
9Machine API gapsCan't automate all typesCheck coverage before promising
10Toggle prerequisitesTransaction imports rejectedEnable toggles after asset creation

Knowledge Check

Question 1

A client asks: "Can Portfolio replace our fund accounting system?" What's the most accurate answer?

Yes -- Portfolio covers all 17 transaction types and has a robust calculation engine
No -- Portfolio covers ~15-20% of PE GL requirements. It handles portfolio reporting and investor servicing, but has no double-entry bookkeeping, chart of accounts, allocation engine, or waterfall calculations
Partially -- it can handle the accounting if you also use the Shared sheet for complex transactions
Correct. Portfolio is a portfolio reporting and investor servicing tool, not a fund accounting system. It produces the right performance metrics (TVPI, DPI, XIRR) but lacks the double-entry bookkeeping, chart of accounts, and allocation engine that fund accounting requires. Setting this expectation correctly is the single most important thing you can do during a client engagement.
Not quite. Portfolio's 17 transaction types and calc engine are strong, but they cover portfolio reporting -- not fund accounting. There's no double-entry bookkeeping, no chart of accounts, no allocation engine, and no waterfall/carried interest calculations. It covers roughly 15-20% of what a PE fund general ledger needs.
Question 2

An import fails with "Row 12. The investor field must exist." You check the database and find the investor record with matching client_reference. What's the most likely cause?

The client_reference has a case mismatch (e.g., "INV-001" vs "inv-001")
The investor record is soft-deleted -- it exists in the database but has a non-null deleted_at timestamp, so the validation's "deleted_at IS NULL" condition excludes it
The Investors sheet is after the current sheet in the workbook
The Machine API doesn't support this transaction type
Correct. This is Gotcha #3. The exists validation rules check deleted_at IS NULL. If the investor was soft-deleted, the record exists in the database but the validation treats it as non-existent. The fix is to either restore the record (clear deleted_at) or use a different client_reference. Option C is also a possible cause, but the question states you found the record in the DB -- so it was already created, meaning sheet order isn't the issue here.
Close, but the key clue is "you check the database and find the investor record." The record exists, yet validation says it doesn't. This points to Gotcha #3: the record is soft-deleted. The exists rules include deleted_at IS NULL, so a soft-deleted record is invisible to validation even though it's physically in the database.

What's Next

You've now seen the honest picture -- what Portfolio does well and where to watch your step. The final module puts everything into practice with hands-on lab exercises where you'll build a fund from scratch, run a direct investment lifecycle, and debug common import failures.