broker-margin-interest-accrual-tracking · v3.0.0 · 2026-09-03 · sha256 ffa6f64f088d0f22

broker-margin-interest-accrual-tracking v3.0.0A

Immutable. This exact content is served forever at /api/v1/blob/ffa6f64f088d0f22.

---
name: broker-margin-interest-accrual-tracking
description: >-
  Use when computing net P&L for a leveraged or short book: tiered blended margin-loan
  rates, short borrow fees on collateral value, 360 versus 365 day counts, and
  calendar-day accrual across weekends and holidays.
license: Apache-2.0
metadata:
  domain: algorithmic-trading
  subdomain: broker-integration
  tags: broker-integration, margin-interest, accrual-tracking, borrowing-cost, pnl-accounting, leverage-cost, institutional
  brokers_frameworks: "Margin Cost Tracker; Python PnL Accounting; Interactive Brokers"
  version: "3.0.0"
  author: algo-trading-skills-contributors
---

## When to Use

Invoke this skill when computing net P&L for a strategy that borrows — one that runs
leverage, carries positions overnight, or sells short. Backtests that ignore financing
overstate returns and Sharpe by the entire cost of the leverage that produced them, and
the overstatement scales with the benchmark rate: a book carrying a permanent debit at a
5% margin rate gives up 5% a year before it makes a single trading decision.

Use it to answer three questions with numbers rather than estimates: what does the debit
balance cost under the broker's **tiered** schedule, what does the borrow cost on the
short book, and what does gross P&L become once both are subtracted.

## When NOT to Use

- **As a substitute for the broker's own interest statement.** This computes an accrual
  from balances you supply. Reconcile it against the monthly posting before trusting it;
  a systematic gap usually means the collateral basis or the day-count is wrong.
- **To model multi-year compounding.** Brokers accrue daily and post monthly (IBKR on
  the third business day of the following month), so interest starts earning interest
  only once posted. This module sums simple daily charges over the window. Negligible
  over weeks, material over years at high rates.
- **To get the net cost of a short.** It reports the **gross** borrow fee. The economic
  cost is the fee less the rebate earned on short sale proceeds, which this does not
  model — so its short-side number is an upper bound, and for general-collateral names a
  loose one.
- **For intraday leverage.** Only end-of-day balances accrue overnight financing. A
  position opened and closed inside the session contributes nothing here.
- **As a live rate source.** Broker tiers are quoted as a spread over a benchmark and
  reprice with monetary policy. The bundled defaults are a dated illustration.

## Prerequisites

- Your broker's **current** margin schedule. IBKR publishes tiers as benchmark + spread
  (Fed Funds for USD), so absolute APRs drift — pull today's table, or build one with
  `tiers_from_benchmark(benchmark_apr, spreads)`.
- The correct day-count for the financing currency: 360 for USD and most currencies at
  IBKR, 365 for exceptions such as GBP.
- Per-security short borrow rates, which are re-struck daily and can move hundreds of
  basis points overnight.
- End-of-day debit balance and gross short market value per date — not an average.
- The exchange holiday calendar, if you want the ledger's accrual blocks to line up with
  settlement days.

## Workflow

1. **Configure the schedule, and let it reject bad input.** Build tiers with
   `tiers_from_benchmark` or explicit `MarginRateTier` values. The constructor validates
   that brackets start at zero, are contiguous, and that the **top tier is open-ended**.
   That last check is not pedantry: a schedule capped at $100k silently prices everything
   above the cap at 0%, so a $200k balance reports 2.5% instead of 5% — half the true
   cost, with no error.

2. **Feed dates, not day counts.** Call `accrue_daily_balances(balances, through_date)`
   with one `EodBalance` per observation. It derives the day count from the dates, so it
   cannot be handed a trading-day count by mistake. Use the scalar
   `calculate_interest_accrual(start_date, holding_days, ...)` only for a constant
   balance, and read `holding_days` as **calendar** days.

3. **Know why weekends cost three days.** Not because settlement is slow — because the
   balance still exists on Saturday and Sunday and interest is computed on the daily
   balance. The consequence is the part people get wrong in both directions: the total
   depends *only* on the number of calendar days in the window. Batching Sat/Sun into
   Friday's ledger row changes granularity, never the total. Applying a weekend
   multiplier on top of a calendar-day count double-charges; feeding a trading-day count
   under-charges by roughly 2/7.

4. **Register holidays for ledger alignment, not for extra cost.** `add_holidays()` makes
   the Friday before a holiday Monday carry one four-day block rather than a three-day
   block plus a row dated on a day the market was shut. The total is identical either
   way; what changes is whether the ledger reconciles line-by-line against the broker's.

5. **Charge the borrow fee on collateral, not market value.** IBKR computes it as
   `value x rate / 360` where value is **102% of the prior day's settlement price,
   rounded up to the next whole dollar**, times shares. Set `short_collateral_markup=1.02`
   to approximate it, or pass an exactly computed `EodBalance.short_collateral_usd` to
   match a statement. The default of 1.0 charges on raw market value and understates.

6. **Subtract both from gross P&L.** `adjusted_net_pnl_usd` is gross less margin interest
   less borrow fees. Report it alongside gross, never instead of it — the gap between the
   two is the number that tells you whether the leverage was worth carrying.

> Full procedure: see `references/workflows.md`.
> Sourced day-count, collateral and settlement conventions: see `references/standards.md`.
> Printable pre-flight checklist: see `assets/checklist.md`.

## Common Pitfalls

- **Counting trading days.** A position held from Monday's close to the following
  Monday's close costs seven days of financing, not five. Over a year of continuous
  carry this understates the cost by roughly 30% — about 2/7 for a weekday count, more
  once holidays are dropped as well.
- **Double-counting the weekend.** The mirror-image error: taking a correct calendar-day
  count and then multiplying Fridays by three. Weekend batching is presentation.
- **Assuming the lowest tier's rate applies to the whole balance.** Blended schedules
  price each slice in its own bracket, so the effective rate falls as the balance grows —
  and a flat-rate assumption misprices in whichever direction the schedule bends.
- **A tier schedule with a capped top bracket.** Anything above the cap is priced at
  zero, and the shortfall grows with the size of the loan. This now raises
  `RateScheduleError` rather than returning a plausible-looking number.
- **Using 365 where the broker uses 360.** It understates the charge by ~1.4% — small per
  day, systematic forever, and it will never reconcile against a statement.
- **Charging borrow on market value.** Collateral is marked at 102% and rounded up, so
  the fee is at least 2% higher than a market-value calculation suggests.
- **Netting a credit cash balance against borrow fees.** A credit balance earns interest
  under a separate, tiered, threshold-gated schedule this module does not model; treating
  it as negative margin interest invents income.
- **Letting NaN through.** A single NaN balance turns net P&L into NaN silently, and a
  financing cost that defaults to zero is indistinguishable from an unlevered strategy —
  it flatters every metric derived from it. Unusable input raises `FinancingDataError`.
- **Hard-coding last year's APRs.** Rates are benchmark-linked; a schedule copied from a
  2023 screenshot was roughly 170bp too high by 2026.
- **Conflating borrow fees with margin interest.** Shorting incurs a borrow fee on the
  short market value *and*, if the account's cash goes into deficit, margin interest on
  that deficit. They are separate charges with separate bases.

## Verification

- Run the unit suite and confirm every test passes:
  `python -m unittest discover -s skills/broker-margin-interest-accrual-tracking/scripts`
- Accrue a constant $100k debit at 5% from a Monday for 7 calendar days and confirm
  $97.22 (`100000 * 0.05 / 360 * 7`), with the Friday row carrying `days_accrued == 3`.
- Run the same balance for 14 days starting on a Monday and on a Friday and confirm the
  totals are identical — if they differ, weekend handling is double-counting.
- Register a holiday Monday and confirm the preceding Friday yields a single four-day
  block and no accrual row is dated on the holiday.
- Construct a tracker whose top tier is finite and confirm `RateScheduleError`; pass NaN
  as a balance and confirm `FinancingDataError`. A number coming back from either is a
  fail-open bug.
- Feed a two-day schedule of $50k then $200k and confirm the effective APR is recomputed
  per day (5% then 4.5%) rather than averaged.
- Reconcile a month of output against the broker's posted interest before relying on it.

## Related Skills

- `short-selling-borrow-cost-and-availability-modeling`
- `broker-account-margin-call-handling`
- `multi-currency-pnl-and-fx-conversion`
- `backtesting-ml-models-against-transaction-costs`
- `execution-realistic-simulation`