Commit Graph

3 Commits

Author SHA1 Message Date
CrossDrain
3e2990a52c feat(ibkr): compute net_market_flows from IBKR equity equity delta and trade flows (#1970)
* feat(ibkr): compute net_market_flows from IBKR equity delta and trade flows

Replace the hardcoded net_market_flows: 0 in HistoricalBalancesSync with an
exact derivation from IBKR's own equity summary data, eliminating any
dependency on third-party security price providers for Period Return.

Formula: nmf = Δnon_cash - net_buy_sell
  - non_cash = IBKR equity total - materializer cash (exact per IBKR)
  - net_buy_sell = sum of trade amounts converted to base currency using
    the stored fx_rate_to_base (IBKR's own FX rate, already on Trade#exchange_rate)

Sets non_cash_adjustments = net_buy_sell so the virtual column identity
(end_non_cash_balance = start + nmf + adjustments) resolves to IBKR's
exact equity figure.

* test(ibkr): add sell-trade and no-trade nmf tests; fix memoization guard

- Add test: sell trades (negative amount) correctly isolate market loss in nmf
- Add test: no-trade scenario produces nmf = full Δnon_cash
- Fix: `return {} unless account` inside ||= exited the method without memoizing;
  restructure to `if account ... else {} end` so the result is always cached

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ibkr): exclude dividend/interest trades from net_buy_sell; use historical FX date

Addresses two issues flagged in code review:

- P1: trades with qty=0 (Dividend, Interest) were included in net_buy_sell,
  inflating/deflating nmf on dates with income events. Filter to qty != 0 at
  the SQL level so only buy/sell trades affect the market-flow calculation.

- P2: Money#exchange_to defaulted to Date.current when no custom_rate was
  stored, causing historical nmf to drift as FX rates change over time.
  Pass date: entry.date so the fallback lookup uses the trade's own date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ibkr): cover Money::ConversionError fallback in trade_flows_by_date

Adds a test that stubs Money#exchange_to to raise ConversionError for a
cross-currency trade with no stored exchange_rate, verifying that the
rescue clause falls back to entry.amount and that nmf and
end_non_cash_balance still resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ibkr): log warning when FX conversion falls back to unconverted amount

When Money::ConversionError is raised for a cross-currency trade with no
stored exchange_rate, warn with entry currency, account currency, date,
amount, and entry/account IDs so the silent fallback is visible in logs.
Same-currency ConversionErrors (unexpected but possible) stay silent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ibkr): skip unconvertible FX trades, redact log, tighten join

- On Money::ConversionError, skip the entry from net_buy_sell rather
  than falling back to the raw amount (which treated e.g. EUR as CHF);
  nmf now absorbs the full Δnon_cash for that date instead of silently
  misstating period return
- Remove entry amount, entry ID, and account ID from the FX warning log
  to avoid exposing financial data in log output
- Consolidate entryable_type guard into the JOIN condition rather than a
  separate WHERE clause
- Add inline comment on the first-day zero case to distinguish intent
  from a bug
- Update ConversionError test to assert skip behavior (nmf=200, not 50)

* fix(ibkr): exclude dates with unconvertible FX trades from balance upsert

* fix(ibkr): skip upsert_all when all balance rows are filtered by failed FX dates

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:48:23 +02:00
CrossDrain
6262b0a493 fix(ibkr): correct historical cash/non-cash split for linked accounts (#1813)
* fix(ibkr): resolve weekend balance oscillations and improve data processing

Address issues where IBKR weekend/holiday data caused incorrect balance
calculations and improve the robustness of IBKR account processing.

- Fix historical balance oscillations by ignoring anomalous weekend rows
  and filling gaps by carrying forward the last known trading day value.
- Normalize report dates to the last trading day to ensure consistency.
- Improve `HoldingsProcessor` to skip individual bad lots instead of
  failing the entire group.
- Refactor `ActivitiesProcessor` to accumulate fee counts locally via
  return values instead of using instance variables.
- Add support for accounting parentheses notation in `DataHelpers`.
- Memoize the account object in `IbkrAccount::Processor` to reduce
  database queries.
- Update tests to reflect date normalization and improved precision
  assertions.

* fix(ibkr): derive historical cash from materializer balances, not equity summary

Real IBKR Flex exports do not include a reliable cash/stock breakdown in
EquitySummaryByReportDateInBase — only the total is consistently present.
The previous implementation parsed the missing cash field as zero and wrote
cash_balance=0 for every historical date, causing negative and wildly
incorrect cash values throughout the account history.

Instead, read the materializer's already-computed cash_balance for each date
(derived from holdings via the reverse calculator) and use only IBKR's total
as an authoritative balance anchor. This is consistent with how present-day
balances are handled and requires no weekend/holiday filtering since IBKR does
not emit weekend rows and holiday totals are legitimate data points.

Also accept equity summary rows without an explicit currency field (some Flex
configurations omit it) and explicitly reject BASE_SUMMARY aggregate rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: simplify boolean coercion in import_commission_transaction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ibkr): cover trailing weekend gap and align qty with valid lots

HistoricalBalancesSync: extend fill_gaps to account.current_anchor_date
so days after the last equity summary row (e.g. Saturday/Sunday when a
sync runs over the weekend) are also overridden rather than left with the
materializer's stale total=cash value.

HoldingsProcessor: replace separate quantity sum + weighted_cost_basis_for
with a single valid_lots method that computes both from the same set of
parseable lots. Previously a lot with a valid position but unparseable
cost_basis_price was excluded from the cost basis calculation but still
counted in quantity, producing inconsistent qty/cost_basis values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* review: address PR feedback on ibkr fix branch

- Remove all "Fix N:" review-artifact comment labels
- Add Sentry.capture_message for silenced anchor repair failure so it surfaces in production monitoring
- Add Rails.logger.warn for zero/nil total rows skipped in HistoricalBalancesSync
- Document normalize_to_last_trading_day holiday limitation and why gap-fill covers it
- Rewrite two non-obvious comments to stand alone without the label prefix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: remove alignment padding in balance_rows hash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ibkr): address two P1 review findings

- Allow zero and negative equity summary totals through HistoricalBalancesSync
  so fully-liquidated and margin accounts are not silently skipped (which would
  cause fill_gaps to propagate a stale non-zero total forward).
- Remove normalize_to_last_trading_day from HoldingsProcessor: shifting weekend
  report_dates to Friday caused Balance::SyncCache#get_holdings_value to find
  no holdings on Saturday/Sunday (exact-date lookup), collapsing non_cash to
  zero — reintroducing the very oscillation the fix was meant to prevent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ibkr): add tests for historical balances sync and data helpers

- Add test case to verify non-cash balance calculation in historical balances sync
- Add test case to ensure rows with unparseable or nil totals are skipped
- Add new test file for IBKR data helpers

* fix(ibkr): prevent date range overflow during historical sync

Adjust the calculation of `last_date` in `HistoricalBalancesSync` to
ensure it does not exceed the current anchor date or today's date.
This prevents the sync process from attempting to fetch or process
future dates, which was causing oscillations in weekend data.

Also remove the conditional check for Sentry before capturing
error messages in the account processor.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:03:04 +02:00
Gian-Reto Tarnutzer
ce5d7dd736 Add Interactive Brokers Provider (#1722)
* Display multi-currency holdings correctly

* Implement IBKR provider

* Fix: Use historical exchange rate for historical prices

* Add brokerage exchange rate for trades

* Sync historical balances from IBKR

* Add logos in activity history

* Fix privacy mode blur in account view

* Improve IBKR XML Flex report parser errors
2026-05-12 23:45:19 +02:00