Skip to content

Accounts

The fundcloud.accounts package wraps account-level data sources — historical NAV, positions, trades, and capital flows from platforms like FundCloud and Interactive Brokers. Every provider satisfies the same AccountProvider protocol, so the analysis surface is identical regardless of source. For task-first walkthroughs, start with Analysing a FundCloud fund or Analysing an Interactive Brokers account.

Provider protocol

fundcloud.accounts._base.AccountProvider

Bases: Protocol

Unified protocol for account-level data providers.

Concrete implementations: :class:fundcloud.accounts.FundCloud and :class:fundcloud.accounts.IB. Future providers (e.g., Plaid) follow the same contract.

Every method returns a :class:pd.DataFrame (or :class:pd.Series where natural) — typed entities stay in docstrings as documented schemas, not in code, to preserve the ecosystem feel of the rest of the library.

capital_flows

capital_flows(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Capital flow events (injections, withdrawals, distributions).

DatetimeIndex (flow_date). Columns: flow_type (INJECTION / WITHDRAWAL / DISTRIBUTION), amount (always positive — direction is encoded in flow_type, not in sign), currency, account_id, notes.

Source code in python/fundcloud/accounts/_base.py
def capital_flows(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Capital flow events (injections, withdrawals, distributions).

    DatetimeIndex (``flow_date``). Columns: ``flow_type``
    (``INJECTION`` / ``WITHDRAWAL`` / ``DISTRIBUTION``), ``amount``
    (always positive — direction is encoded in ``flow_type``, not in
    sign), ``currency``, ``account_id``, ``notes``.
    """
    ...

list_accounts

list_accounts(fund_id: str | None = None) -> pd.DataFrame

One row per linked account, optionally filtered to one fund.

Columns (minimum): account_id, account_name, fund_id, fund_name, currency, external_account_id (broker-side id, when available), latest_nav, latest_aum.

Source code in python/fundcloud/accounts/_base.py
def list_accounts(self, fund_id: str | None = None) -> pd.DataFrame:
    """One row per linked account, optionally filtered to one fund.

    Columns (minimum): ``account_id``, ``account_name``, ``fund_id``,
    ``fund_name``, ``currency``, ``external_account_id`` (broker-side
    id, when available), ``latest_nav``, ``latest_aum``.
    """
    ...

list_funds

list_funds() -> pd.DataFrame

One row per fund visible to this credential.

Columns (minimum): fund_id, name, short_name (where available), currency, inception_date, status, aum, total_shares. Extra provider-specific fields may appear in a trailing info column.

Source code in python/fundcloud/accounts/_base.py
def list_funds(self) -> pd.DataFrame:
    """One row per fund visible to this credential.

    Columns (minimum): ``fund_id``, ``name``, ``short_name``
    (where available), ``currency``, ``inception_date``, ``status``,
    ``aum``, ``total_shares``. Extra provider-specific fields may
    appear in a trailing ``info`` column.
    """
    ...

nav

nav(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame

Historical NAV timeseries.

DatetimeIndex. Columns: nav (per-share), aum (total), shares, daily_return (if reported by the provider). When account_id=None the provider returns the fund-level aggregate; when set, a single-account curve.

adjust_for_flows=True (default) requests a flow-smoothed NAV — implementation-defined per provider (server-side query flag for FundCloud; client-side fallback for IB / Plaid when those land). Pass False for the raw, unadjusted NAV series.

Source code in python/fundcloud/accounts/_base.py
def nav(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame:
    """Historical NAV timeseries.

    DatetimeIndex. Columns: ``nav`` (per-share), ``aum`` (total),
    ``shares``, ``daily_return`` (if reported by the provider).
    When ``account_id=None`` the provider returns the fund-level
    aggregate; when set, a single-account curve.

    ``adjust_for_flows=True`` (default) requests a flow-smoothed
    NAV — implementation-defined per provider (server-side query
    flag for FundCloud; client-side fallback for IB / Plaid
    when those land). Pass ``False`` for the raw, unadjusted
    NAV series.
    """
    ...

positions

positions(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: Timestamp | str | None = None,
) -> pd.DataFrame

Current open positions.

Columns: symbol, name, asset_type, quantity, avg_cost, current_price, market_value, currency, weight, unrealized_pnl, unrealized_pnl_percent, account_id.

Source code in python/fundcloud/accounts/_base.py
def positions(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Current open positions.

    Columns: ``symbol``, ``name``, ``asset_type``, ``quantity``,
    ``avg_cost``, ``current_price``, ``market_value``, ``currency``,
    ``weight``, ``unrealized_pnl``, ``unrealized_pnl_percent``,
    ``account_id``.
    """
    ...

to_portfolio

to_portfolio(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    basis: Basis = "nav_per_share",
    method: ReturnMethod = "total_return",
    benchmark: Series | None = None,
    name: str | None = None,
) -> Portfolio

Fetch NAV (+ flows) and return a ready-to-analyse Portfolio.

basis + method pick the return-computation convention:

  • basis='nav_per_share' + method='total_return' (default): per-share NAV with DISTRIBUTION flows added back; injections and withdrawals are ignored. Matches how funds report investor return.
  • basis='aum' + method='modified_dietz' (or 'daily_twr'): AUM TWR, all flow types signed and aggregated.

Delegates return computation to :func:fundcloud.metrics.returns_from_nav.

Source code in python/fundcloud/accounts/_base.py
def to_portfolio(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    basis: Basis = "nav_per_share",
    method: ReturnMethod = "total_return",
    benchmark: pd.Series | None = None,
    name: str | None = None,
) -> Portfolio:
    """Fetch NAV (+ flows) and return a ready-to-analyse Portfolio.

    ``basis`` + ``method`` pick the return-computation convention:

    - ``basis='nav_per_share'`` + ``method='total_return'`` (default):
      per-share NAV with ``DISTRIBUTION`` flows added back;
      injections and withdrawals are ignored. Matches how funds
      report investor return.
    - ``basis='aum'`` + ``method='modified_dietz'`` (or
      ``'daily_twr'``): AUM TWR, all flow types signed and
      aggregated.

    Delegates return computation to
    :func:`fundcloud.metrics.returns_from_nav`.
    """
    ...

trades

trades(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Executed trades, one row per fill.

DatetimeIndex (trade_date). Columns: symbol, side, quantity, price, amount, currency, fee, broker, status, account_id.

Source code in python/fundcloud/accounts/_base.py
def trades(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Executed trades, one row per fill.

    DatetimeIndex (``trade_date``). Columns: ``symbol``, ``side``,
    ``quantity``, ``price``, ``amount``, ``currency``, ``fee``,
    ``broker``, ``status``, ``account_id``.
    """
    ...

fundcloud.accounts._base.BaseAccountProvider

Bases: ABC

Default implementations shared by every concrete provider.

Subclasses must implement all six fact methods (:meth:list_funds, :meth:list_accounts, :meth:nav, :meth:capital_flows, :meth:positions, :meth:trades). :meth:to_portfolio is provided here and composes them with :meth:Portfolio.from_nav using the correct sign convention for each basis.

_display_name

_display_name(
    fund_id: str | None, account_id: str | None
) -> str

Default Portfolio name for to_portfolio; subclasses can override.

Source code in python/fundcloud/accounts/_base.py
def _display_name(self, fund_id: str | None, account_id: str | None) -> str:
    """Default Portfolio name for ``to_portfolio``; subclasses can override."""
    parts = [self.name]
    if fund_id:
        parts.append(f"fund={fund_id}")
    if account_id:
        parts.append(f"account={account_id}")
    return " ".join(parts)

to_portfolio

to_portfolio(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    basis: Basis = "nav_per_share",
    method: ReturnMethod = "total_return",
    benchmark: Series | None = None,
    name: str | None = None,
) -> Portfolio

Fetch NAV (+ flows) and build a Portfolio.

See :class:AccountProvider.to_portfolio for the parameter semantics. The implementation:

  1. Fetches NAV via :meth:nav.
  2. Fetches capital flows via :meth:capital_flows (skipped when method='none').
  3. Converts flows to the shape expected by :func:fundcloud.metrics.returns_from_nav for the chosen basis (per-share distributions for nav_per_share; signed AUM flows for aum).
  4. Delegates to :meth:Portfolio.from_nav.
Source code in python/fundcloud/accounts/_base.py
def to_portfolio(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    basis: Basis = "nav_per_share",
    method: ReturnMethod = "total_return",
    benchmark: pd.Series | None = None,
    name: str | None = None,
) -> Portfolio:
    """Fetch NAV (+ flows) and build a Portfolio.

    See :class:`AccountProvider.to_portfolio` for the parameter
    semantics. The implementation:

    1. Fetches NAV via :meth:`nav`.
    2. Fetches capital flows via :meth:`capital_flows` (skipped
       when ``method='none'``).
    3. Converts flows to the shape expected by
       :func:`fundcloud.metrics.returns_from_nav` for the chosen
       ``basis`` (per-share distributions for ``nav_per_share``;
       signed AUM flows for ``aum``).
    4. Delegates to :meth:`Portfolio.from_nav`.
    """
    # Always fetch raw NAV here. ``to_portfolio`` applies its own
    # canonical client-side TWR (or ``total_return`` add-back) below
    # via ``returns_from_nav`` / ``Portfolio.from_nav``; passing
    # server-adjusted NAV would double-count the flow correction.
    nav_df = self.nav(
        fund_id,
        account_id=account_id,
        start=start,
        end=end,
        adjust_for_flows=False,
    )
    if nav_df.empty:
        msg = (
            f"No NAV data returned for fund_id={fund_id!r} "
            f"account_id={account_id!r} — cannot build Portfolio."
        )
        raise ValueError(msg)

    display = name or self._display_name(fund_id, account_id)

    if method == "none":
        # No flow adjustment requested.
        nav_series = nav_df["nav"] if basis == "nav_per_share" else nav_df["aum"]
        return Portfolio.from_nav(
            nav_series,
            method="none",
            benchmark=benchmark,
            name=display,
        )

    flows = self.capital_flows(fund_id, account_id=account_id, start=start, end=end)

    if basis == "nav_per_share":
        if method not in ("total_return", "none"):
            msg = (
                f"basis='nav_per_share' is only valid with method='total_return' "
                f"or 'none'; got method={method!r}. Use basis='aum' for "
                f"modified_dietz / daily_twr."
            )
            raise ValueError(msg)
        distributions = _flows_to_per_share_distributions(flows, nav_df["shares"])
        return Portfolio.from_nav(
            nav_df["nav"],
            distributions=distributions,
            method="total_return",
            benchmark=benchmark,
            name=display,
        )

    if basis == "aum":
        if method not in ("modified_dietz", "daily_twr", "none"):
            msg = (
                f"basis='aum' is only valid with method='modified_dietz', "
                f"'daily_twr', or 'none'; got method={method!r}. Use "
                f"basis='nav_per_share' for total_return."
            )
            raise ValueError(msg)
        signed = _flows_to_signed_aum_series(flows, nav_df.index)
        return Portfolio.from_nav(
            nav_df["aum"],
            capital_flows=signed,
            method=method,
            benchmark=benchmark,
            name=display,
        )

    msg = f"unknown basis {basis!r}; expected 'nav_per_share' or 'aum'"
    raise ValueError(msg)

FundCloud provider

fundcloud.accounts.fundcloud.FundCloud

FundCloud(
    fund_id: str | None = None,
    *,
    api_key: str | None = None,
    base_url: str = FUNDCLOUD_BASE_URL,
    timeout: float = 30.0,
)

Bases: BaseAccountProvider

NAV / positions / trades / capital-flows source for FundCloud.

Parameters:

Name Type Description Default
fund_id str | None

Default fund id to use when callers don't pass one. Convenient for the single-fund case. If omitted, each call that needs a fund id auto-resolves: an explicit account_id is mapped to its parent fund via :meth:list_accounts; otherwise the single visible fund is used. :class:fundcloud.errors.AmbiguousError surfaces only when neither hint is available and the credential sees more than one fund.

None
api_key str | None

Falls back to the FUNDCLOUD_API_KEY env var.

None
base_url str

Override the API base URL (useful in tests).

FUNDCLOUD_BASE_URL
timeout float

Per-request timeout in seconds.

30.0
Notes

Every method accepts fund_id and account_id as keywords. With account_id=None the provider returns the fund-level aggregate; with account_id set it drills into a single linked account. You can pass account_id without fund_id — the provider resolves the parent fund automatically (lazy, cached for the provider's lifetime).

:meth:nav requests server-side flow-adjusted NAV by default (adjust_for_flows=True); :meth:to_portfolio always opts out and applies the canonical client-side TWR in :func:fundcloud.metrics.returns_from_nav, which keeps results comparable across providers (IB, Plaid) that don't offer the same server-side flag.

Source code in python/fundcloud/accounts/fundcloud.py
def __init__(
    self,
    fund_id: str | None = None,
    *,
    api_key: str | None = None,
    base_url: str = FUNDCLOUD_BASE_URL,
    timeout: float = 30.0,
) -> None:
    self._default_fund_id = fund_id
    self._client = FundCloudClient(api_key=api_key, base_url=base_url, timeout=timeout)
    # Cache for fund metadata (filled on first list_funds call), used
    # to enrich list_accounts output with fund names.
    self._funds_cache: pd.DataFrame | None = None
    # Lazy-built map of account_id → fund_id, populated when a caller
    # passes only ``account_id=`` and we need to resolve its parent
    # fund. Cached for the provider's lifetime — drop the provider
    # to refresh.
    self._account_to_fund: dict[str, str] | None = None

_account_to_fund_map

_account_to_fund_map() -> dict[str, str]

Lazy-built account_id → fund_id lookup, cached for life of the provider. First call iterates :meth:list_accounts across every visible fund; subsequent calls hit the cache.

Source code in python/fundcloud/accounts/fundcloud.py
def _account_to_fund_map(self) -> dict[str, str]:
    """Lazy-built ``account_id → fund_id`` lookup, cached for life of
    the provider. First call iterates :meth:`list_accounts` across
    every visible fund; subsequent calls hit the cache.
    """
    if self._account_to_fund is None:
        accounts = self.list_accounts()
        self._account_to_fund = {
            str(row["account_id"]): str(row["fund_id"])
            for _, row in accounts.iterrows()
            if row.get("account_id") and row.get("fund_id")
        }
    return self._account_to_fund

_display_name

_display_name(
    fund_id: str | None, account_id: str | None
) -> str

Use fund + account name when available; fall back to ids.

Source code in python/fundcloud/accounts/fundcloud.py
def _display_name(self, fund_id: str | None, account_id: str | None) -> str:
    """Use fund + account name when available; fall back to ids."""
    fid = fund_id if fund_id is not None else self._default_fund_id
    # If the caller passed only account_id, try the lazy map (already
    # populated by an upstream nav() call inside to_portfolio).
    if fid is None and account_id is not None and self._account_to_fund is not None:
        fid = self._account_to_fund.get(account_id)
    if fid is None or self._funds_cache is None:
        return super()._display_name(fund_id, account_id)
    match = self._funds_cache[self._funds_cache["fund_id"] == fid]
    if match.empty:
        return super()._display_name(fund_id, account_id)
    base = str(match.iloc[0]["short_name"] or match.iloc[0]["name"] or fid)
    if account_id:
        base = f"{base} / {account_id}"
    return base

_resolve_fund

_resolve_fund(
    fund_id: str | None, account_id: str | None = None
) -> str

Resolve fund_id via explicit arg → constructor default → account_id lookup → single-fund auto-pick.

When account_id is supplied without an explicit fund_id, we look up the parent fund via :meth:_account_to_fund_map (which lazily fetches and caches :meth:list_accounts). This lets users say src.nav(account_id=X) directly even with multiple funds visible — no need to pass fund_id= for each call.

Source code in python/fundcloud/accounts/fundcloud.py
def _resolve_fund(
    self,
    fund_id: str | None,
    account_id: str | None = None,
) -> str:
    """Resolve ``fund_id`` via explicit arg → constructor default →
    ``account_id`` lookup → single-fund auto-pick.

    When ``account_id`` is supplied without an explicit ``fund_id``,
    we look up the parent fund via :meth:`_account_to_fund_map` (which
    lazily fetches and caches :meth:`list_accounts`). This lets users
    say ``src.nav(account_id=X)`` directly even with multiple funds
    visible — no need to pass ``fund_id=`` for each call.
    """
    if fund_id is not None:
        return fund_id
    if self._default_fund_id is not None:
        return self._default_fund_id
    if account_id is not None:
        mapping = self._account_to_fund_map()
        resolved = mapping.get(account_id)
        if resolved is not None:
            return resolved
        msg = (
            f"account_id={account_id!r} not visible to this credential. "
            f"Pass fund_id= explicitly if the account is known to be valid."
        )
        raise NotFoundError(msg)
    funds = self.list_funds()
    if len(funds) == 0:
        msg = "No funds visible to this credential."
        raise NotFoundError(msg)
    if len(funds) == 1:
        resolved = funds.iloc[0]["fund_id"]
        return str(resolved)
    names = ", ".join(f"{row['name']!r} ({row['fund_id']})" for _, row in funds.iterrows())
    msg = (
        f"Multiple funds visible: {names}. Pass fund_id= to the "
        f"constructor or as a keyword argument to this call."
    )
    raise AmbiguousError(msg)

capital_flows

capital_flows(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Capital flow events — amount is positive; direction is in flow_type.

start defaults to one year before end (today − 1 year when end is also None).

Source code in python/fundcloud/accounts/fundcloud.py
def capital_flows(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Capital flow events — amount is positive; direction is in flow_type.

    ``start`` defaults to one year before ``end`` (today − 1 year
    when ``end`` is also ``None``).
    """
    fid = self._resolve_fund(fund_id, account_id=account_id)
    start = default_start_one_year_back(start, end)
    params: dict[str, Any] = {
        "sort": "flow_date",
        "start_date": _as_date_str(start),
    }
    if account_id is not None:
        params["account_id"] = account_id
    if end is not None:
        params["end_date"] = _as_date_str(end)

    rows = list(self._client.get_paginated(f"/funds/{fid}/capital-flows", params=params))
    if not rows:
        return pd.DataFrame(
            columns=["flow_type", "amount", "currency", "account_id", "notes"],
            index=pd.DatetimeIndex([], name="flow_date"),
        )
    df = pd.DataFrame(
        {
            "flow_type": [r.get("flow_type") for r in rows],
            "amount": [_as_float(r.get("amount")) for r in rows],
            "currency": [r.get("currency") for r in rows],
            "account_id": [r.get("account_id") for r in rows],
            "notes": [r.get("notes") for r in rows],
        },
        index=pd.DatetimeIndex(
            pd.to_datetime([r.get("flow_date") for r in rows]), name="flow_date"
        ),
    ).sort_index()
    return df

list_accounts

list_accounts(fund_id: str | None = None) -> pd.DataFrame

Linked accounts under fund_id (or all funds if None).

Discovered via NAVEntry.account_breakdown on one recent aggregated NAV entry per fund — the FundCloud public API has no dedicated /accounts endpoint.

Source code in python/fundcloud/accounts/fundcloud.py
def list_accounts(self, fund_id: str | None = None) -> pd.DataFrame:
    """Linked accounts under ``fund_id`` (or all funds if None).

    Discovered via ``NAVEntry.account_breakdown`` on one recent
    aggregated NAV entry per fund — the FundCloud public API has
    no dedicated ``/accounts`` endpoint.
    """
    funds_df = self._funds_cache if self._funds_cache is not None else self.list_funds()
    if fund_id is not None:
        funds_df = funds_df[funds_df["fund_id"] == fund_id]
        if funds_df.empty:
            msg = f"fund_id={fund_id!r} not visible to this credential"
            raise NotFoundError(msg)

    rows: list[dict[str, Any]] = []
    for _, fund in funds_df.iterrows():
        fid = fund["fund_id"]
        payload = self._client.get(
            f"/funds/{fid}/nav",
            params={
                "aggregation": "daily",
                "page_size": 1,
                "page": 1,
                "adjust_for_flows": "false",
                "sort": "-date",
            },
        )
        entries = payload.get("data", []) if isinstance(payload, dict) else []
        if not entries:
            continue
        entry = entries[0]
        for acc in entry.get("account_breakdown", []) or []:
            rows.append({
                "account_id": acc.get("account_id"),
                "account_name": acc.get("account_name"),
                "fund_id": fid,
                "fund_name": fund["name"],
                "currency": fund["currency"],
                "external_account_id": acc.get("account_id"),
                "latest_nav": _as_float(acc.get("nav")),
                "latest_aum": _as_float(acc.get("nav")),  # same field on breakdown
            })
    if not rows:
        return pd.DataFrame(
            columns=[
                "account_id",
                "account_name",
                "fund_id",
                "fund_name",
                "currency",
                "external_account_id",
                "latest_nav",
                "latest_aum",
            ],
        )
    return pd.DataFrame(rows)

list_funds

list_funds() -> pd.DataFrame

All funds visible to this credential, as a DataFrame.

Source code in python/fundcloud/accounts/fundcloud.py
def list_funds(self) -> pd.DataFrame:
    """All funds visible to this credential, as a DataFrame."""
    rows = list(self._client.get_paginated("/funds"))
    if not rows:
        empty = pd.DataFrame(
            columns=[
                "fund_id",
                "name",
                "short_name",
                "currency",
                "inception_date",
                "status",
                "aum",
                "total_shares",
                "fund_type",
                "info",
            ],
        )
        self._funds_cache = empty
        return empty
    df = pd.DataFrame({
        "fund_id": [r.get("id") for r in rows],
        "name": [r.get("name") for r in rows],
        "short_name": [r.get("short_name") for r in rows],
        "currency": [r.get("currency") for r in rows],
        "inception_date": pd.to_datetime(
            [r.get("inception_date") for r in rows], errors="coerce"
        ),
        "status": [r.get("status") for r in rows],
        "aum": [_as_float(r.get("aum")) for r in rows],
        "total_shares": [_as_float(r.get("total_shares")) for r in rows],
        "fund_type": [r.get("fund_type") for r in rows],
        "info": [r.get("info") for r in rows],
    })
    self._funds_cache = df
    return df

nav

nav(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame

Historical NAV, fund-aggregated (default) or per-account.

start defaults to one year before end (or today − 1 year when end is also None) — same convention as the network market-data backends. Pass start= explicitly for longer history. adjust_for_flows=True (default) requests the server-side flow-smoothed NAV; pass False for raw values. Both modes always use aggregation=daily (one row per date), which is the only mode compatible with the API's adjust_for_flows=true query.

Source code in python/fundcloud/accounts/fundcloud.py
def nav(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame:
    """Historical NAV, fund-aggregated (default) or per-account.

    ``start`` defaults to one year before ``end`` (or today − 1 year
    when ``end`` is also ``None``) — same convention as the network
    market-data backends. Pass ``start=`` explicitly for longer
    history. ``adjust_for_flows=True`` (default) requests the
    server-side flow-smoothed NAV; pass ``False`` for raw values.
    Both modes always use ``aggregation=daily`` (one row per date),
    which is the only mode compatible with the API's
    ``adjust_for_flows=true`` query.
    """
    fid = self._resolve_fund(fund_id, account_id=account_id)
    start = default_start_one_year_back(start, end)
    params: dict[str, Any] = {
        "aggregation": "daily",
        "adjust_for_flows": "true" if adjust_for_flows else "false",
        "sort": "date",
        "start_date": _as_date_str(start),
    }
    if account_id is not None:
        params["account_id"] = account_id
    if end is not None:
        params["end_date"] = _as_date_str(end)

    rows = list(self._client.get_paginated(f"/funds/{fid}/nav", params=params))
    if not rows:
        return pd.DataFrame(
            columns=["nav", "aum", "shares", "daily_return", "fill_type"],
            index=pd.DatetimeIndex([], name="date"),
        )
    df = pd.DataFrame(
        {
            "nav": [_as_float(r.get("nav")) for r in rows],
            "aum": [_as_float(r.get("aum")) for r in rows],
            "shares": [_as_float(r.get("shares")) for r in rows],
            "daily_return": [_as_float(r.get("daily_return")) for r in rows],
            "fill_type": [r.get("fill_type") for r in rows],
        },
        index=pd.DatetimeIndex(pd.to_datetime([r.get("date") for r in rows]), name="date"),
    ).sort_index()
    return df

positions

positions(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: Timestamp | str | None = None,
) -> pd.DataFrame

Current open positions.

Source code in python/fundcloud/accounts/fundcloud.py
def positions(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: pd.Timestamp | str | None = None,  # reserved for future API support
) -> pd.DataFrame:
    """Current open positions."""
    fid = self._resolve_fund(fund_id, account_id=account_id)
    params: dict[str, Any] = {}
    if account_id is not None:
        params["account_id"] = account_id

    rows = list(self._client.get_paginated(f"/funds/{fid}/positions", params=params))
    if not rows:
        return pd.DataFrame(
            columns=[
                "symbol",
                "name",
                "asset_type",
                "quantity",
                "avg_cost",
                "current_price",
                "market_value",
                "currency",
                "weight",
                "unrealized_pnl",
                "unrealized_pnl_percent",
                "account_id",
                "info",
            ],
        )
    return pd.DataFrame({
        "symbol": [r.get("symbol") for r in rows],
        "name": [r.get("name") for r in rows],
        "asset_type": [r.get("asset_type") for r in rows],
        "quantity": [_as_float(r.get("quantity")) for r in rows],
        "avg_cost": [_as_float(r.get("avg_cost")) for r in rows],
        "current_price": [_as_float(r.get("current_price")) for r in rows],
        "market_value": [_as_float(r.get("market_value")) for r in rows],
        "currency": [r.get("currency") for r in rows],
        "weight": [_as_float(r.get("weight")) for r in rows],
        "unrealized_pnl": [_as_float(r.get("unrealized_pnl")) for r in rows],
        "unrealized_pnl_percent": [_as_float(r.get("unrealized_pnl_percent")) for r in rows],
        "account_id": [r.get("account_name") or r.get("external_account_id") for r in rows],
        "info": [r.get("info") for r in rows],
    })

trades

trades(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Executed trades, one row per fill.

start defaults to one year before end (today − 1 year when end is also None).

Source code in python/fundcloud/accounts/fundcloud.py
def trades(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Executed trades, one row per fill.

    ``start`` defaults to one year before ``end`` (today − 1 year
    when ``end`` is also ``None``).
    """
    fid = self._resolve_fund(fund_id, account_id=account_id)
    start = default_start_one_year_back(start, end)
    params: dict[str, Any] = {
        "sort": "trade_date",
        "start_date": _as_date_str(start),
    }
    if account_id is not None:
        params["account_id"] = account_id
    if end is not None:
        params["end_date"] = _as_date_str(end)

    rows = list(self._client.get_paginated(f"/funds/{fid}/trades", params=params))
    if not rows:
        return pd.DataFrame(
            columns=[
                "symbol",
                "side",
                "quantity",
                "price",
                "amount",
                "currency",
                "fee",
                "broker",
                "status",
                "account_id",
            ],
            index=pd.DatetimeIndex([], name="trade_date"),
        )
    df = pd.DataFrame(
        {
            "symbol": [r.get("symbol") for r in rows],
            "side": [r.get("side") for r in rows],
            "quantity": [_as_float(r.get("quantity")) for r in rows],
            "price": [_as_float(r.get("price")) for r in rows],
            "amount": [_as_float(r.get("amount")) for r in rows],
            "currency": [r.get("currency") for r in rows],
            "fee": [_as_float(r.get("fee")) for r in rows],
            "broker": [r.get("broker") for r in rows],
            "status": [r.get("status") for r in rows],
            "account_id": [r.get("account_name") or r.get("external_account_id") for r in rows],
        },
        index=pd.DatetimeIndex(
            pd.to_datetime([r.get("trade_date") for r in rows]),
            name="trade_date",
        ),
    ).sort_index()
    return df

Interactive Brokers provider

fundcloud.accounts.ib.IB

IB(
    path: str | Path | None = None,
    *,
    files: Sequence[str | Path] | None = None,
    text: str | None = None,
    account_id: str | None = None,
)

Bases: BaseAccountProvider

NAV / capital-flow source backed by an IB Flex Query CSV.

Parameters:

Name Type Description Default
path str | Path | None

Filesystem path to a single Flex Query CSV. Mutually exclusive with files and text.

None
files Sequence[str | Path] | None

A sequence of paths to concatenate (e.g., one file per year, or per sub-account). Each file is parsed independently and the per-section frames are concatenated. Mutually exclusive with path and text.

None
text str | None

Inline CSV content (useful in tests, notebooks, or when piping from another tool). Mutually exclusive with path and files. Equivalent to :meth:from_string.

None
account_id str | None

Default ClientAccountID to use when callers don't pass one. Convenient for the single-account case.

None
Notes

IB has no concept of "fund" — a Flex Query CSV is keyed by ClientAccountID directly. We surface the same id as both fund_id and account_id on the protocol surface, so single- account users don't need to think about the distinction (IB("export.csv").to_portfolio() works zero-arg).

Brokerage accounts also have no shares_outstanding, so the "NAV-per-share + total-return" path FundCloud defaults to is inappropriate for IB. The :meth:to_portfolio default flips to basis="aum" + method="modified_dietz" — the GIPS-standard AUM TWR that's the natural analogue of investor return for a brokerage account.

The :meth:positions and :meth:trades methods return empty frames: the Cash Transactions section is event data, not trades, and the NAV section gives only asset-class subtotals. To populate them, configure separate Open Positions / Trades Flex sections in Interactive Brokers.

Source code in python/fundcloud/accounts/ib.py
def __init__(
    self,
    path: str | Path | None = None,
    *,
    files: Sequence[str | Path] | None = None,
    text: str | None = None,
    account_id: str | None = None,
) -> None:
    provided = sum(1 for v in (path, files, text) if v is not None)
    if provided != 1:
        msg = (
            "Pass exactly one of `path=`, `files=`, or `text=` "
            "(got "
            f"path={path!r}, files={'…' if files else None}, "
            f"text={'…' if text else None})."
        )
        raise ValueError(msg)

    if files is not None:
        exports: list[FlexExport] = [parse_flex_csv(Path(f), require_nav=True) for f in files]
        self._nav_df: pd.DataFrame = pd.concat([e.nav for e in exports]).sort_index()
        self._tx_df: pd.DataFrame = pd.concat([
            e.cash_transactions for e in exports
        ]).sort_index()
    else:
        export = parse_flex_csv(path if path is not None else text)  # type: ignore[arg-type]
        self._nav_df = export.nav
        self._tx_df = export.cash_transactions

    self._default_account_id = account_id

_resolve_account

_resolve_account(
    fund_id: str | None, account_id: str | None
) -> str

Resolve which ClientAccountID to operate on.

Priority: explicit account_id → explicit fund_id → constructor default → unique account in the parsed CSV. Raises :class:AmbiguousError if none are set and the CSV contains multiple accounts.

Source code in python/fundcloud/accounts/ib.py
def _resolve_account(self, fund_id: str | None, account_id: str | None) -> str:
    """Resolve which ``ClientAccountID`` to operate on.

    Priority: explicit ``account_id`` → explicit ``fund_id`` →
    constructor default → unique account in the parsed CSV.
    Raises :class:`AmbiguousError` if none are set and the CSV
    contains multiple accounts.
    """
    if account_id is not None:
        return account_id
    if fund_id is not None:
        return fund_id
    if self._default_account_id is not None:
        return self._default_account_id

    accounts: list[str] = (
        self._nav_df["account_id"].unique().tolist() if not self._nav_df.empty else []
    )
    if len(accounts) == 0:
        msg = "No accounts found in the Flex CSV."
        raise NotFoundError(msg)
    if len(accounts) == 1:
        return str(accounts[0])
    msg = (
        f"Multiple accounts in this Flex CSV: {sorted(accounts)!r}. "
        f"Pass account_id= (or fund_id=) to select one, or set "
        f"`account_id=` on the constructor for a default."
    )
    raise AmbiguousError(msg)

_signed_base_flows

_signed_base_flows(
    account_id: str, nav_index: DatetimeIndex
) -> pd.Series

Return signed base-currency flow per NAV date for one account.

Used by the synthetic adjust_for_flows=True path of :meth:nav. Flows after the last NAV date or before the first are dropped — they cannot be applied within the visible NAV window.

Source code in python/fundcloud/accounts/ib.py
def _signed_base_flows(self, account_id: str, nav_index: pd.DatetimeIndex) -> pd.Series:
    """Return signed base-currency flow per NAV date for one account.

    Used by the synthetic ``adjust_for_flows=True`` path of
    :meth:`nav`. Flows after the last NAV date or before the first
    are dropped — they cannot be applied within the visible NAV
    window.
    """
    sub = self._tx_df[self._tx_df["account_id"] == account_id]
    if sub.empty:
        return pd.Series(0.0, index=nav_index)
    signed = sub["amount_native"].astype(float) * sub["fx_rate_to_base"].astype(float)
    # Group same-day flows; reindex to nav dates (forward-attribute via searchsorted)
    per_date = signed.groupby(signed.index.normalize()).sum()
    nav_sorted = pd.DatetimeIndex(sorted(nav_index.unique()))
    positions = nav_sorted.searchsorted(per_date.index, side="left")
    mask = positions < len(nav_sorted)
    if not mask.any():
        return pd.Series(0.0, index=nav_index)
    target = nav_sorted[positions[mask]]
    aligned = pd.Series(per_date.values[mask], index=target)
    aggregated = aligned.groupby(aligned.index).sum()
    return aggregated.reindex(nav_index).fillna(0.0).astype(float)

capital_flows

capital_flows(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Capital flow events (deposits / withdrawals only).

IB's Flex export ships signed amounts (positive = deposit, negative = withdrawal); we translate to the protocol's flow_type + always-positive amount convention and multiply by FXRateToBase so amounts are denominated in the account's base currency, ready to reconcile against base-currency NAV.

Returns every flow row in the parsed CSV by default — no date-based default filter, since the export window is already fixed by what the user asked IB for. Pass start= / end= to narrow further.

Source code in python/fundcloud/accounts/ib.py
def capital_flows(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Capital flow events (deposits / withdrawals only).

    IB's Flex export ships signed amounts (positive = deposit,
    negative = withdrawal); we translate to the protocol's
    ``flow_type`` + always-positive ``amount`` convention and
    multiply by ``FXRateToBase`` so amounts are denominated in the
    account's base currency, ready to reconcile against
    base-currency NAV.

    Returns every flow row in the parsed CSV by default — no
    date-based default filter, since the export window is already
    fixed by what the user asked IB for. Pass ``start=`` /
    ``end=`` to narrow further.
    """
    acct = self._resolve_account(fund_id, account_id)

    sub = self._tx_df[self._tx_df["account_id"] == acct].sort_index()
    if start is not None or end is not None:
        sub = sub.loc[
            pd.Timestamp(start) if start is not None else None : pd.Timestamp(end)
            if end is not None
            else None
        ]

    if sub.empty:
        return _empty_capital_flows()

    signed_native = sub["amount_native"].astype(float)
    fx = sub["fx_rate_to_base"].astype(float)
    signed_base = signed_native * fx
    flow_type = signed_base.where(signed_base < 0, other="INJECTION").where(
        signed_base >= 0, other="WITHDRAWAL"
    )
    # Resolve base currency from the NAV section if we have it; otherwise
    # fall back to the native currency.
    base_ccy = self._account_base_currency(acct) or sub["currency"].iloc[0]

    return pd.DataFrame(
        {
            "flow_type": flow_type.to_numpy(),
            "amount": signed_base.abs().to_numpy(),
            "currency": base_ccy,
            "account_id": sub["account_id"].to_numpy(),
            "notes": sub["description"].to_numpy(),
        },
        index=sub.index,
    )

from_string classmethod

from_string(
    text: str, *, account_id: str | None = None
) -> IB

Construct an :class:IB provider from inline CSV text.

Source code in python/fundcloud/accounts/ib.py
@classmethod
def from_string(cls, text: str, *, account_id: str | None = None) -> IB:
    """Construct an :class:`IB` provider from inline CSV text."""
    return cls(text=text, account_id=account_id)

list_accounts

list_accounts(fund_id: str | None = None) -> pd.DataFrame

One row per linked account.

For IB, there is one account per ClientAccountID and the fund id is the same id; the result matches :meth:list_funds with the column names normalized to the AccountProvider account schema.

Source code in python/fundcloud/accounts/ib.py
def list_accounts(self, fund_id: str | None = None) -> pd.DataFrame:
    """One row per linked account.

    For IB, there is one account per ``ClientAccountID`` and the
    fund id is the same id; the result matches :meth:`list_funds`
    with the column names normalized to the ``AccountProvider``
    account schema.
    """
    funds = self.list_funds()
    if fund_id is not None:
        funds = funds[funds["fund_id"] == fund_id]
        if funds.empty:
            msg = f"fund_id={fund_id!r} not present in this Flex CSV"
            raise NotFoundError(msg)
    if funds.empty:
        return pd.DataFrame(
            columns=[
                "account_id",
                "account_name",
                "fund_id",
                "fund_name",
                "currency",
                "external_account_id",
                "latest_nav",
                "latest_aum",
            ],
        )
    return pd.DataFrame({
        "account_id": funds["fund_id"],
        "account_name": funds["name"],
        "fund_id": funds["fund_id"],
        "fund_name": funds["name"],
        "currency": funds["currency"],
        "external_account_id": funds["fund_id"],
        "latest_nav": funds["aum"],
        "latest_aum": funds["aum"],
    }).reset_index(drop=True)

list_funds

list_funds() -> pd.DataFrame

One row per unique ClientAccountID in the parsed export.

For IB, fund and account are the same id (single-tier hierarchy). The columns match the AccountProvider protocol so generic UI / report code that walks list_funds() works identically across providers.

Source code in python/fundcloud/accounts/ib.py
def list_funds(self) -> pd.DataFrame:
    """One row per unique ``ClientAccountID`` in the parsed export.

    For IB, fund and account are the same id (single-tier
    hierarchy). The columns match the ``AccountProvider`` protocol
    so generic UI / report code that walks ``list_funds()`` works
    identically across providers.
    """
    if self._nav_df.empty:
        return pd.DataFrame(
            columns=[
                "fund_id",
                "name",
                "short_name",
                "currency",
                "inception_date",
                "status",
                "aum",
                "total_shares",
            ],
        )

    rows: list[dict[str, object]] = []
    for acct_id, sub in self._nav_df.groupby("account_id"):
        sub_sorted = sub.sort_index()
        currencies = sub_sorted["currency"].unique().tolist()
        if len(currencies) > 1:
            msg = (
                f"IB account {acct_id!r} reports NAV in multiple "
                f"currencies {currencies!r}; one base currency per "
                f"account is required. Re-run the Flex Query in a "
                f"single base currency."
            )
            raise MalformedDataError(msg)
        rows.append({
            "fund_id": acct_id,
            "name": acct_id,
            "short_name": acct_id,
            "currency": currencies[0],
            "inception_date": sub_sorted.index.min(),
            "status": "ACTIVE",
            "aum": float(sub_sorted["aum"].iloc[-1]),
            "total_shares": 1.0,
        })
    return pd.DataFrame(rows)

nav

nav(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame

Daily NAV / AUM rows from the Flex Query NAV section.

Unlike network providers (e.g., :class:fundcloud.accounts.fundcloud.FundCloud) this method does not apply a 1-year-back default to start. The Flex Query CSV is already bounded by whatever period was requested at export time — returning the full file is almost always the right behaviour. Pass start= / end= to narrow the window further.

adjust_for_flows=True (default) returns a synthetic flow-adjusted AUM curve computed client-side via :func:fundcloud.metrics.returns_from_nav, replaying the return series as if no deposits or withdrawals had occurred. Pass False for the raw Total column straight from the CSV.

Returns:

Type Description
DataFrame

DatetimeIndex named date. Columns: nav (= AUM, since IB has no per-share concept), aum, shares (synthesised constant 1.0), daily_return, fill_type. daily_return reflects the chosen adjust_for_flows mode.

Source code in python/fundcloud/accounts/ib.py
def nav(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    adjust_for_flows: bool = True,
) -> pd.DataFrame:
    """Daily NAV / AUM rows from the Flex Query NAV section.

    Unlike network providers (e.g.,
    :class:`fundcloud.accounts.fundcloud.FundCloud`) this method
    does **not** apply a 1-year-back default to ``start``. The
    Flex Query CSV is already bounded by whatever period was
    requested at export time — returning the full file is
    almost always the right behaviour. Pass ``start=`` / ``end=``
    to narrow the window further.

    ``adjust_for_flows=True`` (default) returns a synthetic
    flow-adjusted AUM curve computed client-side via
    :func:`fundcloud.metrics.returns_from_nav`, replaying the
    return series as if no deposits or withdrawals had occurred.
    Pass ``False`` for the raw ``Total`` column straight from the
    CSV.

    Returns
    -------
    pd.DataFrame
        DatetimeIndex named ``date``. Columns: ``nav`` (= AUM,
        since IB has no per-share concept), ``aum``, ``shares``
        (synthesised constant ``1.0``), ``daily_return``,
        ``fill_type``. ``daily_return`` reflects the chosen
        ``adjust_for_flows`` mode.
    """
    from fundcloud.metrics import returns_from_nav

    acct = self._resolve_account(fund_id, account_id)

    sub = self._nav_df[self._nav_df["account_id"] == acct].sort_index()
    if start is not None or end is not None:
        sub = sub.loc[
            pd.Timestamp(start) if start is not None else None : pd.Timestamp(end)
            if end is not None
            else None
        ]

    if sub.empty:
        return pd.DataFrame(
            columns=["nav", "aum", "shares", "daily_return", "fill_type"],
            index=pd.DatetimeIndex([], name="date"),
        )

    aum = sub["aum"].astype(float)

    if adjust_for_flows and len(aum) > 1:
        # Drop leading rows where AUM is non-positive (account is
        # pre-funding). Those are technically valid Total values
        # straight from IB but they break compounding (anchor of
        # 0 → flat-zero curve, division by zero in pct_change).
        funded_mask = aum > 0
        if funded_mask.any():
            first_funded = aum.index[funded_mask.argmax()]
            aum_funded = aum.loc[first_funded:]
            signed_flows = self._signed_base_flows(acct, aum_funded.index)
            try:
                returns = returns_from_nav(
                    aum_funded, capital_flows=signed_flows, method="daily_twr"
                )
            except ValueError:
                returns = pd.Series(dtype=float)
            if not returns.empty:
                # Replay AUM curve as if no flows had occurred — anchor at
                # the first funded AUM, compound flow-adjusted returns.
                anchor = float(aum_funded.iloc[0])
                synthetic = pd.Series(index=aum_funded.index, dtype=float)
                synthetic.iloc[0] = anchor
                synthetic.iloc[1:] = anchor * (1.0 + returns).cumprod()
                # Stitch leading non-funded rows back so the result still
                # spans the user-visible NAV period.
                leading = aum.loc[:first_funded].iloc[:-1]
                aum = pd.concat([leading, synthetic])

    return pd.DataFrame(
        {
            "nav": aum.to_numpy(),
            "aum": aum.to_numpy(),
            "shares": 1.0,
            "daily_return": pd.Series(aum, dtype=float).pct_change().to_numpy(),
            "fill_type": "actual",
        },
        index=aum.index,
    )

positions

positions(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: Timestamp | str | None = None,
) -> pd.DataFrame

Returns an empty frame.

The Flex Query "Net Asset Value (NAV) in Base" section provides asset-class subtotals (Stock, Bonds, Crypto, …) but not per-symbol position rows. Configure a separate "Open Positions" Flex section in Interactive Brokers to populate this.

Source code in python/fundcloud/accounts/ib.py
def positions(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    asof: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Returns an empty frame.

    The Flex Query "Net Asset Value (NAV) in Base" section provides
    asset-class subtotals (`Stock`, `Bonds`, `Crypto`, …) but not
    per-symbol position rows. Configure a separate "Open Positions"
    Flex section in Interactive Brokers to populate this.
    """
    return pd.DataFrame(
        columns=[
            "symbol",
            "name",
            "asset_type",
            "quantity",
            "avg_cost",
            "current_price",
            "market_value",
            "currency",
            "weight",
            "unrealized_pnl",
            "unrealized_pnl_percent",
            "account_id",
            "info",
        ],
    )

to_portfolio

to_portfolio(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
    basis: Basis = "aum",
    method: ReturnMethod = "modified_dietz",
    benchmark: Series | None = None,
    name: str | None = None,
) -> Portfolio

Build a :class:Portfolio from the IB account.

Defaults differ from the FundCloud provider: basis="aum" + method="modified_dietz". Brokerage accounts have no shares_outstanding, so the "NAV-per-share + total-return" path doesn't apply — AUM-basis Modified Dietz is the GIPS-standard analogue of investor return for a brokerage book. Override either kwarg to switch (e.g., method="daily_twr" for daily-precision flow timing).

Source code in python/fundcloud/accounts/ib.py
def to_portfolio(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
    basis: Basis = "aum",
    method: ReturnMethod = "modified_dietz",
    benchmark: pd.Series | None = None,
    name: str | None = None,
) -> Portfolio:
    """Build a :class:`Portfolio` from the IB account.

    Defaults differ from the FundCloud provider: ``basis="aum"`` +
    ``method="modified_dietz"``. Brokerage accounts have no
    ``shares_outstanding``, so the "NAV-per-share + total-return"
    path doesn't apply — AUM-basis Modified Dietz is the
    GIPS-standard analogue of investor return for a brokerage book.
    Override either kwarg to switch (e.g., ``method="daily_twr"``
    for daily-precision flow timing).
    """
    return super().to_portfolio(
        fund_id,
        account_id=account_id,
        start=start,
        end=end,
        basis=basis,
        method=method,
        benchmark=benchmark,
        name=name,
    )

trades

trades(
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: Timestamp | str | None = None,
    end: Timestamp | str | None = None,
) -> pd.DataFrame

Returns an empty frame.

The Flex Query "Cash Transactions" section is event data, not trades. Configure a separate "Trades" Flex section in Interactive Brokers to populate this.

Source code in python/fundcloud/accounts/ib.py
def trades(
    self,
    fund_id: str | None = None,
    *,
    account_id: str | None = None,
    start: pd.Timestamp | str | None = None,
    end: pd.Timestamp | str | None = None,
) -> pd.DataFrame:
    """Returns an empty frame.

    The Flex Query "Cash Transactions" section is event data, not
    trades. Configure a separate "Trades" Flex section in
    Interactive Brokers to populate this.
    """
    return pd.DataFrame(
        columns=[
            "symbol",
            "side",
            "quantity",
            "price",
            "amount",
            "currency",
            "fee",
            "broker",
            "status",
            "account_id",
        ],
        index=pd.DatetimeIndex([], name="trade_date"),
    )

Errors

Provider errors are rooted at fundcloud.errors.FundcloudError — see the errors reference for the full hierarchy.