跳转至

Performance return semantics

fincore.performance is the canonical performance domain. Its APIs make the return convention, cashflow timing, fee treatment, and currency conversion explicit so a report never silently selects a financial interpretation.

Cashflow-adjusted time-weighted returns

Provide valuations in the reporting currency on a unique, increasing timezone-aware DatetimeIndex, with strictly positive capital at every period opening. A terminal zero valuation is allowed to represent a total loss. A positive external cashflow is a contribution into the portfolio; a negative one is a withdrawal. Every flow must have the same date as a valuation because an unvalued intra-period flow has no defensible timing.

import pandas as pd

from fincore.performance.cashflows import cashflow_adjusted_returns, cashflow_adjusted_twr

dates = pd.to_datetime(["2024-01-31", "2024-02-29", "2024-03-31"], utc=True)
valuations = pd.Series([100.0, 110.0, 121.0], index=dates)
cashflows = pd.Series([10.0], index=[dates[1]])  # contribution at 2024-02-29

period_returns = cashflow_adjusted_returns(valuations, cashflows, timing="end")
total_return = cashflow_adjusted_twr(valuations, cashflows, timing="end")

assert period_returns.round(12).tolist() == [0.0, 0.1]
assert round(total_return, 12) == 0.1

For an end-of-period flow, the period return is (V_end + fee_if_gross - flow) / V_start - 1. For a start-of-period flow it is (V_end + fee_if_gross) / (V_start + flow) - 1. Choose timing="start" only when the operational record supports that convention; fincore never infers it from a timestamp alone. If a valuation record has mixed start- and end-of-period flows, pass a cashflow_timings Series with one "start" or "end" value for every nonzero cashflow date; a partial timing ledger is rejected rather than silently falling back to the scalar policy.

When multiple transactions share one valuation timestamp, do not net them before calculation: use an event ledger with one row per transaction. Its timezone-aware index must match the valuations and its only columns are amount and timing; every row declares "start" or "end". A single cashflow_currency still applies to the whole request, so normalize a mixed-currency ledger before calling this API.

ledger = pd.DataFrame(
    {"amount": [10.0, -5.0], "timing": ["start", "end"]},
    index=[dates[1], dates[1]],
)
one_period = cashflow_adjusted_twr(
    pd.Series([100.0, 116.0], index=dates[:2]),
    ledger,
)
assert round(one_period, 12) == 0.1

Fees and currencies

Returns are net-of-fees by default: the fee is already reflected in the ending valuation. Use fee_treatment="gross" only with an explicit fee series in the reporting currency to add those fees back for a gross result.

Cashflows in a different currency require a full FX series whose index is exactly the valuation index. FX values mean reporting-currency units per one cashflow-currency unit. Missing FX, unvalued flow dates, nonpositive capital, and ambiguous timing are errors rather than silently adjusted values.

Scope and disclosure

TWR measures the compound return after external-flow neutralization; mwr/xirr measure money-weighted return with a separately documented, conservative conventional-cashflow policy. Every enhanced strategy report now renders a calculation disclosure. With a plain periodic return series, its default is deliberately conservative: it says that cashflow and fee treatment were not supplied and no cashflow adjustment was performed. It does not silently label that series as TWR.

Pass a DisclosureContext only when the calculation record supports the declarations. Its established defaults are themselves declarations: TWR, gross-of-fees, no cashflows and annualized metrics. Therefore, treat every context instance as a complete caller assertion, including when only one field is overridden. Omit disclosure_context entirely to receive conservative values derived from the validated report input. A legacy precomputed report model without a disclosure is rendered only with its immutable model metadata and explicit legacy/unknown provenance; later raw inputs are not consulted. The resolved, structured disclosure is rendered in HTML/PDF. An optional audit manifest records its sanitized form, redacting credentials and omitting local paths rather than copying sensitive free-form text verbatim.

import pandas as pd

from fincore.performance.disclosures import DisclosureContext
from fincore.report import create_strategy_report

returns = pd.Series(
    [0.001 if day % 2 else -0.0005 for day in range(60)],
    index=pd.date_range("2024-01-02", periods=60, freq="B", tz="UTC"),
)
context = DisclosureContext(
    convention="TWR after external-flow neutralization",
    return_type="simple",
    units="decimal return per period",
    frequency="daily",
    fees="net-of-fees",
    cashflows="timed transaction ledger",
    benchmark="S&P 500 total return",
    risk_free="USD 3M Treasury",
)

# ``returns`` is the already-calculated, validated periodic return series.
artifacts = create_strategy_report(
    returns,
    output="report.html",
    disclosure_context=context,
    return_result=True,
    audit_manifest=True,
)
assert artifacts.model["performance_disclosure"]["convention"].startswith("TWR")

These helpers provide GIPS-aware calculation and disclosure support; they do not certify GIPS compliance.