wps260 d74e9caf7b Optimize and Fix provider price fetches for sold securities and batch queries (#1580)
* Performance and bug fixes in provider price fetches

Three distinct bugs caused the price provider API to be called unnecessarily
on every investment account sync.

1. Sold securities triggered a provider call on every sync forever

   import_security_prices passed end_date: Date.current for every security
   ever traded. Security::Price::Importer short-circuits via all_prices_exist?
   only when persisted_count == expected_count, where:

     expected_count = (clamped_start_date..Date.current).count

   This range increases daily, so a security closed two years ago would have
   all historical prices in the DB unnecessarily.  This also causes any closed
   securities to fetch prices daily, forever.

   Fix: separate currently-held securities (end_date: Date.current) from
   historical-only securities (end_date: last holding date for that security).
   Once a closed position's price range is complete through its last holding
   date, all_prices_exist? becomes permanently stable and no further provider
   calls occur for that security.

   "Currently held" is defined as appearing in account.current_holdings, which
   returns the most recent holding per security with qty != 0. On the first
   sync after a sell, the pre-sale holding is still the most recent, so the
   security correctly receives end_date: Date.current for one final sync before
   the new qty=0 holding is materialised.

2. Offline securities were not filtered

   account.trades.map(&:security) returned all securities regardless of the
   offline flag. This results in fetching of securities even if the provider
   cannot serve them, or if the user don't want them served for some reason
   (eg when there are symbol collisions that causes the wrong prices to be
   returned) The global MarketDataImporter correctly uses Security.online;
   the account-scoped importer did not.

   Fix: Security.online.where(id: all_security_ids) matches the established
   contract. Offline IDs still pass through the pluck step but resolve to nil
   in the securities hash and are skipped by the existing `next unless security`
   guard.

3. N+1 queries for security loading and per-security start dates

   - account.trades.map(&:security): triggered one SQL query per trade to load
     the security association (N+1).
   - first_required_price_date(security): issued 2 DB queries per security -
     one MIN(entries.date) and one EXISTS - so S securities = 2S queries.

   Fix: replace with batch queries totalling 4 regardless of security count:
   - account.current_holdings.pluck(:security_id) - current security IDs
   - account.trades.pluck(:security_id).uniq - traded security IDs
   - Security.online.where(id: ...) - load all security records at once
   - batch_first_required_price_dates: one GROUP BY security_id MIN(entries.date)
     over trades, one pluck for provider-holding security IDs, one GROUP BY
     security_id MAX(date) over holdings for historical end dates

* fix(market-data-importer): fetch prices through today for reopened positions

Account::Syncer runs import_market_data before materialize_balances, so
current_holdings reflects the last materialized snapshot rather than the
current trade state. If a security was previously sold (stale holdings show
qty=0) and then repurchased in the same sync cycle, it landed in
historical_ids and had its end_date capped at the old last_holding_date.
This caused all_prices_exist? to short-circuit, skipping the price fetch
through today, and leaving the forthcoming holding materialization without
a price for the repurchase period.

Fix: compare the latest trade date against the last holding date for each
historical security. If the trade is newer, the position was reopened before
holdings were rematerialized; treat end_date as Date.current for that sync.
The cap still applies on subsequent syncs once materialize_balances has
updated the holdings table.

Adds a regression test covering the repurchase scenario.

* hoist account.start_date out of per-security loop

Account#start_date issues SELECT MIN(date) FROM entries on every call.
Inside batch_first_required_price_dates it was called up to twice per
security (holding_date assignment + fallback), producing up to 2N extra
queries for an account with N provider-held securities.

Cache the result in account_start_date before the loop.

* assert offline securities are skipped

Adds a regression test verifying that Account::MarketDataImporter never
calls fetch_security_prices for a security with offline: true, covering
the Security.online filter on line 54 of the importer.
2026-05-01 23:40:33 +02:00
2026-04-13 13:44:37 +02:00
2026-04-19 11:16:04 +02:00
2024-02-02 09:05:04 -06:00
2024-02-02 09:05:04 -06:00
2024-02-02 09:05:04 -06:00
2025-05-20 13:31:05 -05:00
2024-02-02 09:05:04 -06:00
2024-08-23 10:06:24 -04:00
2024-08-23 10:06:24 -04:00
2024-02-02 09:05:04 -06:00
2024-02-02 09:05:04 -06:00
2025-09-24 00:19:51 +02:00
2026-04-13 13:44:37 +02:00
2024-02-02 09:05:04 -06:00
2026-05-01 01:58:37 +02:00

Ask DeepWiki View performance data on Skylight Dosu Pipelock Security Scan

sure_shot

Deutsch | Español | Français | 日本語 | 한국어 | Português | Русский | 中文

Sure: The personal finance app for everyone

Get involved: DiscordWebsiteIssues

Important

This repository is a community fork of the now-abandoned Maybe Finance project.
Learn more in their final release doc.

Backstory

The Maybe Finance team spent most of 20212022 building a full-featured personal finance and wealth management app. It even included an “Ask an Advisor” feature that connected users with a real CFP/CFA — all included with your subscription.

The business end of things didn't work out, and so they stopped developing the app in mid-2023.

After spending nearly $1 million on development (employees, contractors, data providers, infra, etc.), the team open-sourced the app. Their goal was to let users self-host it for free — and eventually launch a hosted version for a small fee.

They actually did launch that hosted version … briefly.

That also didnt work out — at least not as a sustainable B2C business — so now here we are: hosting a community-maintained fork to keep the codebase alive and see where this can go next.

Join us!

Hosting Sure

Sure is a fully working personal finance app that can be self hosted with Docker.

Forking and Attribution

This repo is a community fork of the archived Maybe Finance repo. Youre free to fork it under the AGPLv3 license — but wed love it if you stuck around and contributed here instead.

To stay compliant and avoid trademark issues:

  • Be sure to include the original AGPLv3 license and clearly state in your README that your fork is based on Maybe Finance but is not affiliated with or endorsed by Maybe Finance Inc.
  • "Maybe" is a trademark of Maybe Finance Inc. and therefore, use of it is NOT allowed in forked repositories (or the logo)

Performance Issues

With data-heavy apps, inevitably, there are performance issues. We've set up a public dashboard showing the problematic requests seen on the demo site, along with the stacktraces to help debug them.

https://www.skylight.io/app/applications/s6PEZSKwcklL/recent/6h/endpoints

Any contributions that help improve performance are very much welcome.

Local Development Setup

If you are trying to self-host the app, read this guide to get started.

The instructions below are for developers to get started with contributing to the app.

Requirements

  • See .ruby-version file for required Ruby version
  • PostgreSQL >9.3 (latest stable version recommended)
  • Redis > 5.4 (latest stable version recommended)

Getting Started

cd sure
cp .env.local.example .env.local
bin/setup
bin/dev

# Optionally, load demo data
rake demo_data:default

Visit http://localhost:3000 to view the app.

If you loaded the optional demo data, log in with these credentials:

  • Email: user@example.com
  • Password: Password1!

For further instructions, see guides below.

Setup Guides

One-click Install

Run on PikaPods

Deploy on Railway

Managed OpenClaw for Sure Finances

Managed OpenClaw for Sure Finances

License and Trademarks

Maybe and Sure are both distributed under an AGPLv3 license.

  • "Maybe" is a trademark of Maybe Finance, Inc.
  • "Sure" is not, and refers to this community fork.

Alt

Description
No description provided
Readme AGPL-3.0 80 MiB
Languages
Ruby 74.4%
HTML 16%
Dart 5.7%
JavaScript 3.2%
CSS 0.3%
Other 0.2%