# Wealthfolio Wealthfolio is an open-source, private, and local-first portfolio tracker. Keep your financial data on your device, with optional cloud sync via Connect. Site: https://wealthfolio.app ================================================================================ DOCUMENTATION ================================================================================ --- # Wealthfolio Addon Development Source: https://wealthfolio.app/docs/addons
Wealthfolio addons are TypeScript modules that extend the application's functionality through a sandboxed runtime. This guide covers how to build, test, and distribute addons for Wealthfolio 3.6 and later. **New to addon development?** Start with our [Quick Start Guide](/docs/addons/getting-started) to create your first addon. ## What are Wealthfolio Addons? Addons are TypeScript/React-based extensions that provide access to Wealthfolio's financial data and UI system through a brokered API bridge. **Technical Foundation** Each addon is an ES module that exports an `enable(ctx)` function. Wealthfolio loads the module inside a sandboxed iframe and passes an `AddonContext` object with access to APIs, route rendering, events, logging, secrets, and network access. **Integration Capabilities** Addons declare their pages and navigation in `manifest.json` (`contributes.routes` + `contributes.links`), so the host builds the sidebar and knows every route **without running addon code**. Routes must live under the addon's own namespace, such as `/addons/hello-world` for `hello-world-addon`. An addon with declared routes boots **lazily** — its `enable(ctx)` runs on the first visit to one of its routes, then renders inside its own iframe. **Development Environment** Built with TypeScript, React, and modern web APIs. Includes hot-reload development server, comprehensive type definitions, and host-provided dependencies to keep addon bundles small. ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ Wealthfolio Host Application │ ├─────────────────────────────────────────────────────────────────┤ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Addon Runtime │ │ Permission │ │ API Bridge │ │ │ │ │ │ System │ │ │ │ │ │ • Load/Unload │ │ • Detection │ │ • Type Bridge │ │ │ │ • Lifecycle │ │ • Validation │ │ • Domain APIs │ │ │ │ • Context Mgmt │ │ • Enforcement │ │ • Scoped Access │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ Individual Addons │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Addon A │ │ Addon B │ │ Addon C │ │ Addon D │ │ │ │ │ │ │ │ │ │ │ │ │ │ enable() │ │ enable() │ │ enable() │ │ enable() │ │ │ │ onDisable │ │ onDisable │ │ onDisable │ │ onDisable │ │ │ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │ │ │ API Calls │ │ API Calls │ │ API Calls │ │ API Calls │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Basic Addon Structure Every addon exports an enable function that receives a context object: ```typescript import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk'; import { Button } from '@wealthfolio/ui'; function MyAddonPage({ ctx }: { ctx: AddonContext }) { return (

My Tool

); } // The host owns a single React root per addon and mounts the route `component` // itself. Capture the context at enable time so the route wrapper can hand it // down. (Do NOT call createRoot yourself — the host manages the lifecycle.) let addonCtx: AddonContext | undefined; const MyAddonRoute = () => ( ); const enable: AddonEnableFunction = (ctx) => { addonCtx = ctx; // The sidebar entry and route are declared in manifest.json `contributes`, so // the host builds navigation before this addon boots. The runtime route `id` // MUST match `contributes.routes[].id`. ctx.router.add({ id: 'my-addon', path: '/addons/my-addon', component: MyAddonRoute, }); // Listen to events let unlistenPortfolio: (() => void) | undefined; void ctx.api.events.portfolio .onUpdateComplete(() => { // Handle portfolio updates }) .then((unlisten) => { unlistenPortfolio = unlisten; }); // Cleanup. The host owns the React root, so there is no root to unmount. ctx.onDisable(() => { addonCtx = undefined; unlistenPortfolio?.(); }); }; export default enable; ``` ## Permission System Addons operate under a permission-based security model with three stages: #### 1. Static Analysis During installation, addon code is scanned for API usage patterns: ```typescript // This pattern is detected: const accounts = await ctx.api.accounts.getAll(); // Detected permission: accounts.getAll ``` #### 2. Permission Categories | Category | Risk Level | Common Functions | | --------------------- | ---------- | ----------------------------------------------------------- | | `accounts` | High | getAll, create | | `portfolio` | High | getHoldings, getHolding, update, recalculate, valuations | | `activities` | High | getAll, search, create, update, saveMany, import | | `market-data` | Low | searchTicker, syncHistory, sync, getProviders, dividends | | `assets` | Medium | getProfile, updateProfile, updateQuoteMode | | `quotes` | Low | update, getHistory | | `performance` | Medium | calculateHistory, calculateSummary, calculateAccountsSimple | | `currency` | Low | getAll, update, add | | `financial-planning` | Medium | getAll, create, update, getFunding, saveFunding | | `contribution-limits` | Medium | getAll, create, update, calculateDeposits | | `settings` | Medium | get, update, backupDatabase | | `files` | Medium | openCsvDialog, openSaveDialog | | `snapshots` | High | getAll, getByDate, save, checkImport, importSnapshots | | `events` | Low | onDrop, onUpdateComplete, onSyncComplete | | `network` | High | request | | `secrets` | High | set, get, use, delete | **Baseline capabilities are implicit.** `ui` (sidebar/router/navigation), `query`, `toast`, `logger`, and `storage` are granted to every addon and are **not** declared in `permissions`. Only the data domains above plus `files`, `network`, `secrets`, `events`, `snapshots`, and `settings` need a declaration. #### 3. User Approval During installation, users see both declared and detected permissions, plus declared network hosts. They can approve or reject the addon installation. Updates that add permissions require reinstall approval. ## Available APIs The addon context provides access to host APIs through a sandbox bridge: ```typescript interface AddonContext { ui: { root: HTMLElement; }; sidebar: SidebarAPI; router: RouterAPI; onDisable: (callback: () => void) => void; api: { accounts: AccountsAPI; portfolio: PortfolioAPI; activities: ActivitiesAPI; market: MarketAPI; assets: AssetsAPI; quotes: QuotesAPI; performance: PerformanceAPI; exchangeRates: ExchangeRatesAPI; goals: GoalsAPI; contributionLimits: ContributionLimitsAPI; settings: SettingsAPI; files: FilesAPI; snapshots: SnapshotsAPI; events: EventsAPI; secrets: SecretsAPI; storage: StorageAPI; network: NetworkAPI; navigation: NavigationAPI; query: QueryAPI; toast: ToastAPI; logger: LoggerAPI; }; } ``` ## Development Setup ### Required Packages ```bash npm install @wealthfolio/addon-sdk @wealthfolio/ui react react-dom npm install -D @wealthfolio/addon-dev-tools typescript vite ``` ### Core Dependencies - **@wealthfolio/addon-sdk**: TypeScript types and API definitions - **@wealthfolio/ui**: UI components based on shadcn/ui and Tailwind CSS - **@wealthfolio/addon-dev-tools**: CLI and development server ### Development Server The development tools include a hot-reload server: ```bash # Start development server npm run dev:server # Available on localhost:3001-3003 # Auto-discovered by Wealthfolio ``` ``` Development Server Structure: ├─ /health # Health check ├─ /status # Build status ├─ /manifest.json # Addon manifest └─ /addon.js # Built addon code ``` ## Project Structure ``` hello-world-addon/ ├── src/ │ ├── addon.tsx # Main addon entry point │ ├── components/ # React components │ ├── hooks/ # React hooks │ ├── pages/ # Addon pages │ ├── utils/ # Utility functions │ └── types/ # Type definitions ├── assets/ # Static assets (optional) ├── dist/ # Built files (generated) ├── manifest.json # Addon metadata and permissions ├── package.json # NPM package configuration ├── vite.config.ts # Build configuration ├── tsconfig.json # TypeScript configuration └── README.md # Documentation ``` ### Manifest File ```json { "id": "my-addon", "name": "My Addon", "version": "1.0.0", "main": "dist/addon.js", "description": "Addon description", "author": "Your Name", "sdkVersion": "3.6.1", "minWealthfolioVersion": "3.6.1", "enabled": true, "contributes": { "routes": [{ "id": "my-addon", "path": "/addons/my-addon" }], "links": { "sidebar": [ { "id": "my-addon", "route": "my-addon", "label": "My Addon", "icon": "squares-four", "order": 100 } ] } }, "permissions": [], "hostDependencies": { "@wealthfolio/addon-sdk": "^3.6.1", "@wealthfolio/ui": "^3.6.0", "react": "^19.2.0", "react-dom": "^19.2.0" } } ``` A **route** (`contributes.routes`) is a durable addon page the host can render before the addon boots; a **link** (`contributes.links`, only the `"sidebar"` slot is consumed today) places a declared route in a host slot. The runtime `router.add({ id })` must use the same `id` as its declared route. Navigation and the baseline `ui` capability need no `permissions` entry. ## Lifecycle Management ### Installation Process ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ │ │ ZIP File │───▶│ Extract │───▶│ Validate │───▶│ Analyze │ │ │ │ │ │ │ │ Permissions │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ │ │ Running │◀───│ Enable │◀───│ Load │◀─────────────┘ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ ``` 1. **Extract**: Unzip addon package and read files 2. **Validate**: Check manifest.json structure and compatibility 3. **Analyze Permissions**: Scan code for API usage patterns 4. **Approve**: User reviews permissions and requested network hosts 5. **Load**: Create sandbox iframe with scoped APIs 6. **Enable**: Call addon's enable function 7. **Running**: Addon functionality is active ### Lazy Activation Addons that declare `contributes.routes` boot **lazily**: the host reads the manifest and builds the sidebar and route table without executing any addon code, then calls `enable(ctx)` only when one of the addon's routes is first visited. Because navigation comes from the manifest, a route survives reloads and deep links even before the addon has run. Sandbox failures degrade gracefully rather than crashing the host — blocked storage, an unknown API call, or an unavailable route surface as a toast plus an inline panel in the addon's frame, and errors in one addon never affect another. ### Context Isolation Each addon runs in its own sandboxed iframe and receives an isolated context. Host APIs are exposed through a broker; addons do not receive host React globals, the raw host context, or another addon's context. ```typescript // Addon "my-addon" accessing its own secrets await ctx.api.secrets.set('api-key', 'value'); const apiKey = await ctx.api.secrets.get('api-key'); ``` Secrets are stored in the OS keyring under the addon's own scope. Other addons cannot read them. ## UI Components Addons have access to Wealthfolio's UI component library: ```typescript import { Button, Card, Dialog, Input, Table } from '@wealthfolio/ui'; import { AmountDisplay, GainAmount, CurrencyInput } from '@wealthfolio/ui/financial'; import { TrendingUp, DollarSign } from 'lucide-react'; function MyComponent() { return (
Portfolio Growth
); } ``` Available libraries: - All Radix UI components - **Financial components** (`components/financial`) for amounts, gains, and currency inputs - Lucide React and Phosphor icons — for rendering **inside your own page** (any icon you like) - Tailwind CSS utilities - Recharts for data visualization - React Query for data fetching - date-fns for date manipulation The **sidebar** icon is different: it's named by a string from a curated set (typed as `AddonIconName`), not a component you import. See [Sidebar icons](/docs/addons/api-reference#sidebar-icons) for the full list. ## Build and Distribution ### Build Configuration Use the addon dev tools template when possible. It configures host dependencies so common libraries are provided by Wealthfolio instead of bundled into every addon. ```typescript // vite.config.ts export default defineConfig({ plugins: [react()], build: { lib: { entry: 'src/addon.tsx', fileName: () => 'addon.js', formats: ['es'], }, rollupOptions: { external: [ 'react', 'react-dom', 'react-dom/client', '@wealthfolio/addon-sdk', '@wealthfolio/ui', ], }, }, }); ``` ### Package Scripts ```json { "scripts": { "build": "vite build", "dev": "vite build --watch", "dev:server": "wealthfolio dev", "clean": "rm -rf dist", "package": "zip -r $npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md -x \"*.map\"", "bundle": "pnpm clean && pnpm build && pnpm package", "lint": "tsc --noEmit", "type-check": "tsc --noEmit" } } ``` ## Error Handling ### Addon Failures - Errors are logged but don't affect other addons - Host application continues normally - Users see error notifications ### Permission Violations - `PermissionError` thrown for unauthorized API calls - API calls are blocked - Errors are logged for debugging ## Security Model - Each addon runs in a sandboxed iframe with a brokered API bridge - Routes are limited to the addon's own `/addon/` or `/addons/` path - Manifest permissions are enforced at runtime - Static analysis detects additional API usage during installation - Network access is brokered by the backend and limited to approved HTTPS hosts - Secrets are stored in the OS keyring and scoped by addon ID - Bearer authorization headers are injected by the backend from add-on-scoped secrets - Addon network requests are audited locally ## Publishing Users can install addons directly from ZIP files. To publish your addon in the Wealthfolio Store, contact **wealthfolio@teymz.com**. ## Quick Start

🏃‍♂️ Quick Start

Create your first addon

Get Started →

📖 API Reference

Explore available APIs

Browse APIs →

💡 Examples

See real addon implementations

Browse Examples →

🏪 Addon Store

Explore available addons

Visit Store →
--- # API Reference Source: https://wealthfolio.app/docs/addons/api-reference
# API Reference Complete reference for APIs available to Wealthfolio addons. All functions require appropriate permissions in `manifest.json`. ## Context Overview The `AddonContext` is provided to your addon's `enable` function: ```typescript export interface AddonContext { ui: { root: HTMLElement; }; sidebar: SidebarAPI; router: RouterAPI; onDisable: (callback: () => void) => void; api: HostAPI; } ``` Basic usage: ```typescript export default function enable(ctx: AddonContext) { // Access APIs from event handlers, effects, or async helpers void ctx.api.accounts.getAll().then((accounts) => { ctx.api.logger.info(`Loaded ${accounts.length} accounts`); }); // UI integration ctx.sidebar.addItem({ /* ... */ }); ctx.router.add({ /* ... */ }); // Cleanup ctx.onDisable(() => { // cleanup code }); } ``` ## API Domains The API is organized into host-brokered domains: | Domain | Description | Key Functions | | ----------------------- | ----------------------- | ------------------------------------------------------------------------- | | **Accounts** | Account management | `getAll`, `create` | | **Portfolio** | Holdings and valuations | `getHoldings`, `getIncomeSummary`, `update`, `recalculate` | | **Activities** | Trading transactions | `getAll`, `create`, `import`, `search`, `update` | | **Market** | Market data and symbols | `searchTicker`, `sync`, `getProviders`, `fetchDividends` | | **Performance** | Performance metrics | `calculateHistory`, `calculateSummary` | | **Assets** | Asset profiles | `getProfile`, `updateProfile`, `updateQuoteMode` | | **Quotes** | Price quotes | `update`, `getHistory` | | **Goals** | Financial goals | `getAll`, `create`, `update`, `getFunding`, `saveFunding` | | **Contribution Limits** | Investment limits | `getAll`, `create`, `update`, `calculateDeposits` | | **Exchange Rates** | Currency rates | `getAll`, `update`, `add` | | **Settings** | App configuration | `get`, `update`, `backupDatabase` | | **Files** | File operations | `openCsvDialog`, `openSaveDialog` | | **Snapshots** | Holdings snapshots | `getAll`, `getByDate`, `save`, `checkImport`, `importSnapshots`, `delete` | | **Events** | Real-time events | `onUpdateComplete`, `onSyncComplete`, `onDrop` | | **Secrets** | Secure storage | `get`, `set`, `delete` | | **Storage** | Durable key-value store | `get`, `set`, `delete` | | **Network** | Brokered HTTPS requests | `request` | | **Logger** | Logging operations | `error`, `info`, `warn`, `debug`, `trace` | | **Navigation** | Route navigation | `navigate` | | **Query** | React Query integration | `getClient`, `invalidateQueries`, `refetchQueries` | | **Toast** | User notifications | `success`, `error`, `warning`, `info` | ## Accounts API Manage user accounts with full CRUD operations. ### `getAll(): Promise` Retrieves all user accounts. ```typescript const accounts = await ctx.api.accounts.getAll(); interface Account { id: string; name: string; currency: string; isActive: boolean; totalValue?: number; createdAt: string; updatedAt: string; } ``` #### `create(account: AccountCreate): Promise` Creates a new account with validation. ```typescript const newAccount = await ctx.api.accounts.create({ name: 'My Investment Account', currency: 'USD', isActive: true, }); ``` **Data Validation**: Account creation includes validation for currency codes, name uniqueness, and other business rules. --- ## Portfolio API Access portfolio data, holdings, and performance information with real-time updates. ### Methods #### `getHoldings(accountId: string): Promise` Gets all holdings for a specific account with current valuations. ```typescript const holdings = await ctx.api.portfolio.getHoldings('account-123'); // Example holding structure interface Holding { id: string; accountId: string; symbol: string; quantity: number; marketValue: number; totalCost: number; averagePrice: number; gainLoss: number; gainLossPercent: number; currency: string; assetType: 'Stock' | 'ETF' | 'Bond' | 'Crypto' | 'Other'; } ``` #### `getHolding(accountId: string, assetId: string): Promise` Gets a specific holding with detailed information. ```typescript const holding = await ctx.api.portfolio.getHolding('account-123', 'AAPL'); ``` #### `update(): Promise` Triggers a portfolio update/recalculation across all accounts. ```typescript await ctx.api.portfolio.update(); ``` #### `recalculate(): Promise` Forces a complete portfolio recalculation from scratch. ```typescript await ctx.api.portfolio.recalculate(); ``` #### `getIncomeSummary(): Promise` Gets comprehensive income summary across all accounts. ```typescript const income = await ctx.api.portfolio.getIncomeSummary(); ``` #### `getHistoricalValuations(accountId?: string, startDate?: string, endDate?: string): Promise` Gets historical portfolio valuations for charts and analysis. ```typescript const history = await ctx.api.portfolio.getHistoricalValuations( 'account-123', // optional, omit for all accounts '2024-01-01', '2024-12-31' ); ``` #### `getLatestValuations(accountIds: string[]): Promise` Gets latest valuations for a set of accounts. ```typescript const valuations = await ctx.api.portfolio.getLatestValuations(['account-1', 'account-2']); ``` --- ## Activities API Manage trading activities, transactions, and data imports with advanced search capabilities. ### Methods #### `getAll(accountId?: string): Promise` Gets all activities, optionally filtered by account. ```typescript // All activities across all accounts const allActivities = await ctx.api.activities.getAll(); // Activities for specific account const accountActivities = await ctx.api.activities.getAll('account-123'); ``` #### `search(page: number, pageSize: number, filters: ActivitySearchFilters, searchKeyword: string, sort?: ActivitySort): Promise` Advanced search with pagination and filters. ```typescript const results = await ctx.api.activities.search( 1, // page 50, // pageSize { // filters accountIds: ['account-123'], activityTypes: ['BUY'], symbol: 'AAPL', }, 'AAPL', // searchKeyword { id: 'date', desc: true } // sort ); ``` #### `create(activity: ActivityCreate): Promise` Creates a new activity with validation. ```typescript const activity = await ctx.api.activities.create({ accountId: 'account-123', activityType: 'BUY', symbol: 'AAPL', quantity: 100, unitPrice: 150.5, currency: 'USD', date: '2024-12-01', isDraft: false, }); ``` #### `update(activity: ActivityUpdate): Promise` Updates an existing activity with conflict detection. ```typescript const updated = await ctx.api.activities.update({ ...existingActivity, quantity: 150, unitPrice: 145.75, }); ``` #### `saveMany(request: ActivityBulkMutationRequest): Promise` Efficiently creates, updates, or deletes multiple activities in a single transaction. ```typescript const result = await ctx.api.activities.saveMany({ creates: [ { accountId: 'account-123', activityType: 'BUY' /* ... */ }, { accountId: 'account-123', activityType: 'DIVIDEND' /* ... */ }, ], updates: [], deleteIds: [], }); ``` #### `import(activities: ActivityImport[]): Promise` Imports validated activities with duplicate detection. ```typescript const imported = await ctx.api.activities.import(checkedActivities); ``` #### `checkImport(activities: ActivityImport[]): Promise` Validates activities before import with error reporting. ```typescript const validated = await ctx.api.activities.checkImport(activities); ``` #### `getImportMapping(accountId: string): Promise` Get import mapping configuration for an account. ```typescript const mapping = await ctx.api.activities.getImportMapping('account-123'); ``` #### `saveImportMapping(mapping: ImportMappingData): Promise` Save import mapping configuration. ```typescript const savedMapping = await ctx.api.activities.saveImportMapping(mapping); ``` ### Activity Types Reference | Type | Use Case | Cash Impact | Holdings Impact | | -------------- | ---------------------- | -------------- | ------------------ | | `BUY` | Purchase securities | Decreases cash | Increases quantity | | `SELL` | Dispose of securities | Increases cash | Decreases quantity | | `DIVIDEND` | Cash dividend received | Increases cash | No change | | `INTEREST` | Interest earned | Increases cash | No change | | `DEPOSIT` | Add funds | Increases cash | No change | | `WITHDRAWAL` | Remove funds | Decreases cash | No change | | `TRANSFER_IN` | Assets moved in | Varies | Increases quantity | | `TRANSFER_OUT` | Assets moved out | Varies | Decreases quantity | | `FEE` | Brokerage fees | Decreases cash | No change | | `TAX` | Taxes paid | Decreases cash | No change | --- ## Market Data API Access market data, search symbols, and sync with external providers. ### Methods > Throughout the Market, Assets, and Quotes APIs, `assetId` is an asset's identifier. For a > listed security this is its symbol (e.g. `AAPL`); cash uses `$CASH-`. #### `searchTicker(query: string): Promise` Search for ticker symbols across multiple data providers. ```typescript const results = await ctx.api.market.searchTicker('AAPL'); ``` #### `syncHistory(): Promise` Syncs historical market data for all portfolio holdings. ```typescript await ctx.api.market.syncHistory(); ``` #### `sync(assetIds: string[], refetchAll: boolean): Promise` Syncs market data for specific assets with cache control. ```typescript // Sync latest data (uses cache if recent) await ctx.api.market.sync(['AAPL', 'MSFT', 'GOOGL'], false); // Force refresh all data await ctx.api.market.sync(['AAPL', 'MSFT', 'GOOGL'], true); ``` #### `getProviders(): Promise` Gets available market data providers and their status. ```typescript const providers = await ctx.api.market.getProviders(); ``` #### `fetchDividends(symbol: string, options?: FetchDividendsOptions): Promise` Fetches dividend history for a symbol. ```typescript const dividends = await ctx.api.market.fetchDividends('AAPL', { startDate: '2024-01-01', endDate: '2024-12-31', }); ``` --- ## Assets API Access and manage asset profiles and data sources. ### Methods #### `getProfile(assetId: string): Promise` Gets detailed asset profile information. ```typescript const asset = await ctx.api.assets.getProfile('AAPL'); ``` #### `updateProfile(payload: UpdateAssetProfile): Promise` Updates asset profile information. ```typescript const updatedAsset = await ctx.api.assets.updateProfile({ id: 'AAPL', name: 'Apple Inc.', kind: 'INVESTMENT', // ... displayCode, notes, quoteMode, providerConfig }); ``` #### `updateQuoteMode(assetId: string, quoteMode: string): Promise` Switches an asset between market-fetched and manual quotes (`MARKET` or `MANUAL`). ```typescript const asset = await ctx.api.assets.updateQuoteMode('AAPL', 'MANUAL'); ``` --- ## Quotes API Manage price quotes and historical data. ### Methods #### `update(assetId: string, quote: Quote): Promise` Updates quote information for an asset. ```typescript await ctx.api.quotes.update('AAPL', { symbol: 'AAPL', price: 150.5, date: '2024-12-01', // ... other quote data }); ``` #### `getHistory(assetId: string): Promise` Gets historical quotes for an asset. ```typescript const history = await ctx.api.quotes.getHistory('AAPL'); ``` --- ## Performance API Calculate portfolio and account performance metrics with historical analysis. ### Methods #### `calculateHistory(itemType: 'account' | 'symbol', itemId: string, startDate?: string, endDate?: string): Promise` Calculates detailed performance history for charts and analysis. ```typescript const history = await ctx.api.performance.calculateHistory( 'account', 'account-123', '2024-01-01', '2024-12-31' ); ``` #### `calculateSummary(args: { itemType: 'account' | 'symbol'; itemId: string; startDate?: string | null; endDate?: string | null; }): Promise` Calculates comprehensive performance summary with key metrics. ```typescript const summary = await ctx.api.performance.calculateSummary({ itemType: 'account', itemId: 'account-123', startDate: '2024-01-01', endDate: '2024-12-31', }); ``` #### `calculateAccountsSimple(accountIds: string[]): Promise` Calculates simple performance metrics for multiple accounts efficiently. ```typescript const performance = await ctx.api.performance.calculateAccountsSimple([ 'account-123', 'account-456', ]); ``` --- ## Exchange Rates API Manage currency exchange rates for multi-currency portfolios. ### Methods #### `getAll(): Promise` Gets all exchange rates. ```typescript const rates = await ctx.api.exchangeRates.getAll(); ``` #### `update(updatedRate: ExchangeRate): Promise` Updates an existing exchange rate. ```typescript const updatedRate = await ctx.api.exchangeRates.update({ id: 'rate-123', fromCurrency: 'USD', toCurrency: 'EUR', rate: 0.85, // ... other rate data }); ``` #### `add(newRate: Omit): Promise` Adds a new exchange rate. ```typescript const newRate = await ctx.api.exchangeRates.add({ fromCurrency: 'USD', toCurrency: 'GBP', rate: 0.75, // ... other rate data }); ``` --- ## Contribution Limits API Manage investment contribution limits and calculations. ### Methods #### `getAll(): Promise` Gets all contribution limits. ```typescript const limits = await ctx.api.contributionLimits.getAll(); ``` #### `create(newLimit: NewContributionLimit): Promise` Creates a new contribution limit. ```typescript const limit = await ctx.api.contributionLimits.create({ name: 'RRSP 2024', limitType: 'RRSP', maxAmount: 30000, year: 2024, // ... other limit data }); ``` #### `update(id: string, updatedLimit: NewContributionLimit): Promise` Updates an existing contribution limit. ```typescript const updatedLimit = await ctx.api.contributionLimits.update('limit-123', { name: 'Updated RRSP 2024', maxAmount: 31000, // ... other updated data }); ``` #### `calculateDeposits(limitId: string): Promise` Calculates deposits for a specific contribution limit. ```typescript const deposits = await ctx.api.contributionLimits.calculateDeposits('limit-123'); ``` --- ## Goals API Manage financial goals and funding rules. ### Methods #### `getAll(): Promise` Gets all goals. ```typescript const goals = await ctx.api.goals.getAll(); ``` #### `create(goal: unknown): Promise` Creates a new goal. ```typescript const goal = await ctx.api.goals.create({ name: 'Retirement Fund', targetAmount: 500000, targetDate: '2040-01-01', // ... other goal data }); ``` #### `update(goal: Goal): Promise` Updates an existing goal. ```typescript const updatedGoal = await ctx.api.goals.update({ ...existingGoal, targetAmount: 600000, }); ``` #### `getFunding(goalId: string): Promise` Gets funding rules for a specific goal. ```typescript const funding = await ctx.api.goals.getFunding('goal-123'); ``` #### `saveFunding(goalId: string, rules: GoalAllocation[]): Promise` Saves funding rules for a specific goal. ```typescript const savedFunding = await ctx.api.goals.saveFunding('goal-123', [ { goalId: 'goal-123', accountId: 'account-456', percentage: 50 }, // ... other funding rules ]); ``` `getAllocations()` and `updateAllocations()` still exist as deprecated compatibility shims. Prefer `getFunding(goalId)` and `saveFunding(goalId, rules)` for new addons. --- ## Settings API Manage application settings and configuration. ### Methods #### `get(): Promise` Gets application settings. ```typescript const settings = await ctx.api.settings.get(); ``` #### `update(settingsUpdate: Partial): Promise` Updates application settings. ```typescript const updatedSettings = await ctx.api.settings.update({ ...currentSettings, baseCurrency: 'EUR', // ... other settings }); ``` #### `backupDatabase(): Promise<{ filename: string }>` Creates a database backup. ```typescript const backup = await ctx.api.settings.backupDatabase(); ``` --- ## Files API Handle file operations and dialogs. ### Methods #### `openCsvDialog(): Promise` Opens a CSV file selection dialog. ```typescript const files = await ctx.api.files.openCsvDialog(); if (files) { // Process selected files } ``` #### `openSaveDialog(fileContent: Uint8Array | Blob | string, fileName: string): Promise` Opens a file save dialog. ```typescript const result = await ctx.api.files.openSaveDialog(fileContent, 'export.csv'); ``` --- ## Snapshots API Manage holdings snapshots for accounts that use holdings tracking mode. ### Methods #### `getAll(accountId: string, dateFrom?: string, dateTo?: string): Promise` Gets snapshots for an account, optionally filtered by date range. ```typescript const snapshots = await ctx.api.snapshots.getAll('account-123', '2024-01-01', '2024-12-31'); ``` #### `getByDate(accountId: string, date: string): Promise` Gets holdings from a snapshot date. ```typescript const holdings = await ctx.api.snapshots.getByDate('account-123', '2024-12-31'); ``` #### `save(accountId: string, holdings: SnapshotHoldingInput[], cashBalances: Record, snapshotDate?: string): Promise` Saves a holdings snapshot. ```typescript await ctx.api.snapshots.save( 'account-123', [{ symbol: 'AAPL', quantity: '10', currency: 'USD' }], { USD: '250.00' }, '2024-12-31' ); ``` #### `checkImport(accountId: string, snapshots: SnapshotInput[]): Promise` Validates snapshot import data before saving. ```typescript const preview = await ctx.api.snapshots.checkImport('account-123', parsedSnapshots); ``` #### `importSnapshots(accountId: string, snapshots: SnapshotInput[]): Promise` Imports validated snapshots. ```typescript const imported = await ctx.api.snapshots.importSnapshots('account-123', parsedSnapshots); ``` #### `delete(accountId: string, date: string): Promise` Deletes a snapshot for a date. ```typescript await ctx.api.snapshots.delete('account-123', '2024-12-31'); ``` --- ## Secrets API Securely store and retrieve sensitive data like API keys and tokens. Secrets are stored in the OS keyring and scoped to your addon ID. ### Methods #### `set(key: string, value: string): Promise` Stores a secret value encrypted and scoped to your addon. ```typescript // Store API key securely await ctx.api.secrets.set('api-key', 'your-secret-api-key'); // Store user credentials await ctx.api.secrets.set('auth-token', userAuthToken); ``` #### `get(key: string): Promise` Retrieves a secret value (returns null if not found). ```typescript const apiKey = await ctx.api.secrets.get('api-key'); if (apiKey) { // Use the value locally inside this addon only. // For HTTP Authorization headers, prefer ctx.api.network.request({ auth }). } ``` #### `delete(key: string): Promise` Permanently deletes a secret. ```typescript await ctx.api.secrets.delete('old-api-key'); ``` **Security Note**: Secrets are scoped by addon ID. Other addons cannot access your secrets, and you cannot access theirs. For brokered network auth, declare `secrets.use` and pass `auth: { type: 'bearer' | 'basic', secretKey: '...' }`; Wealthfolio injects the `Authorization` header on the backend. --- ## Storage API Durable per-addon key-value storage. Use this instead of `localStorage`, which is unavailable in the sandboxed (opaque-origin) iframe and throws. Values are opaque strings owned by your addon: storage survives addon updates, is cleared on uninstall, replicates across paired devices, and is scoped so no other addon can read it. Storage is a **baseline capability** — it needs no `permissions` entry. ### Methods #### `get(key: string): Promise` Retrieves a stored value (resolves to `null` if not set). ```typescript const raw = await ctx.api.storage.get('prefs'); const prefs = raw ? JSON.parse(raw) : defaults; ``` #### `set(key: string, value: string): Promise` Stores a string value. Keys are ≤ 128 characters and limited to the charset `[A-Za-z0-9_.:-]`. Values are capped at roughly 250 KB each — because storage replicates across a user's paired devices, `set` rejects an oversized value instead of failing to sync later. Use many small keys rather than one large blob, and keep device-local caches out of storage. ```typescript await ctx.api.storage.set('prefs', JSON.stringify(prefs)); ``` #### `delete(key: string): Promise` Permanently deletes a stored value. ```typescript await ctx.api.storage.delete('prefs'); ``` --- ## Network API Send brokered HTTPS requests to hosts declared in `manifest.json`. Browser `fetch` still has normal CORS restrictions and should not be used for secrets or authorization headers. ### Manifest ```json { "permissions": [ { "category": "network", "functions": ["request"], "purpose": "Fetch market data from api.example.com" }, { "category": "secrets", "functions": ["use"], "purpose": "Inject the user's API token into brokered requests" } ], "network": { "allowedHosts": ["api.example.com"] } } ``` `allowedHosts` is declared by the addon. `approvedHosts` is managed by Wealthfolio after the user approves hosts during install or update. ### `request(request: NetworkRequest): Promise` ```typescript const response = await ctx.api.network.request({ url: 'https://api.example.com/v1/quotes', method: 'GET', headers: { Accept: 'application/json', }, auth: { type: 'bearer', secretKey: 'api-token', }, }); if (response.status === 200) { const payload = JSON.parse(response.body); } ``` `auth.type` selects the scheme Wealthfolio injects: - `'bearer'` → `Authorization: Bearer ` - `'basic'` → `Authorization: Basic `. Store the already base64-encoded `user:pass` string as the secret; the broker only prefixes the scheme. This is the way to reach APIs like [SimpleFin Bridge](https://www.simplefin.org) whose access URLs embed credentials — extract `user:pass` at setup, base64-encode it, and save it with `ctx.api.secrets.set`. Network requests are constrained: - HTTPS only - Host must match a declared and user-approved host - Local, private, link-local, and metadata IPs are blocked - Redirects are disabled - Request bodies are limited to 1 MB - Response bodies are limited to 2 MB - `Authorization` cannot be supplied in `headers`; use `auth.secretKey` - Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` --- ## Logger API Provides logging functionality with automatic addon prefix. ### Methods #### `error(message: string): void` Logs an error message. ```typescript ctx.api.logger.error('Failed to fetch data from API'); ``` #### `info(message: string): void` Logs an informational message. ```typescript ctx.api.logger.info('Data sync completed successfully'); ``` #### `warn(message: string): void` Logs a warning message. ```typescript ctx.api.logger.warn('API rate limit approaching'); ``` #### `debug(message: string): void` Logs a debug message. ```typescript ctx.api.logger.debug('Processing 100 activities'); ``` #### `trace(message: string): void` Logs a trace message for detailed debugging. ```typescript ctx.api.logger.trace('Entering function processActivity'); ``` --- ## Event System Listen to real-time events for responsive addon behavior. The listener methods return promises. Inside `enable(ctx)`, keep `enable` synchronous and store cleanup functions when the listener promises resolve: ```typescript export default function enable(ctx: AddonContext) { let unlistenPortfolio: (() => void) | undefined; void ctx.api.events.portfolio .onUpdateComplete(() => { refreshPortfolioData(); }) .then((unlisten) => { unlistenPortfolio = unlisten; }); ctx.onDisable(() => { unlistenPortfolio?.(); }); } ``` ### Portfolio Events #### `onUpdateStart(callback: EventCallback): Promise` Fires when portfolio update starts. ```typescript const unlistenStart = await ctx.api.events.portfolio.onUpdateStart((event) => { console.log('Portfolio update started'); showLoadingIndicator(); }); ``` #### `onUpdateComplete(callback: EventCallback): Promise` Fires when portfolio calculations are updated. ```typescript const unlistenPortfolio = await ctx.api.events.portfolio.onUpdateComplete((event) => { console.log('Portfolio updated:', event.payload); // Refresh your addon's data refreshPortfolioData(); }); // Clean up on disable ctx.onDisable(() => { unlistenPortfolio(); }); ``` #### `onUpdateError(callback: EventCallback): Promise` Fires when portfolio update encounters an error. ```typescript const unlistenError = await ctx.api.events.portfolio.onUpdateError((event) => { console.error('Portfolio update failed:', event.payload); showErrorMessage(); }); ``` ### Market Events #### `onSyncStart(callback: EventCallback): Promise` Fires when market data sync starts. ```typescript const unlistenSyncStart = await ctx.api.events.market.onSyncStart(() => { console.log('Market sync started'); showSyncIndicator(); }); ``` #### `onSyncComplete(callback: EventCallback): Promise` Fires when market data sync is completed. ```typescript const unlistenMarket = await ctx.api.events.market.onSyncComplete(() => { console.log('Market data updated!'); // Update price displays updatePriceDisplays(); }); ``` ### Import Events #### `onDropHover(callback: EventCallback): Promise` Fires when files are hovered over for import. ```typescript const unlistenHover = await ctx.api.events.import.onDropHover((event) => { console.log('File hover detected'); showDropZone(); }); ``` #### `onDrop(callback: EventCallback): Promise` Fires when files are dropped for import. ```typescript const unlistenImport = await ctx.api.events.import.onDrop((event) => { console.log('File dropped:', event.payload); // Trigger import workflow handleFileImport(event.payload.files); }); ``` #### `onDropCancelled(callback: EventCallback): Promise` Fires when file drop is cancelled. ```typescript const unlistenCancel = await ctx.api.events.import.onDropCancelled(() => { console.log('File drop cancelled'); hideDropZone(); }); ``` --- ## Navigation API Navigate programmatically within the Wealthfolio application. ### Methods #### `navigate(route: string): Promise` Navigate to a specific route in the application. ```typescript // Navigate to a specific account await ctx.api.navigation.navigate('/accounts/account-123'); // Navigate to portfolio overview await ctx.api.navigation.navigate('/portfolio'); // Navigate to activities page await ctx.api.navigation.navigate('/activities'); // Navigate to settings await ctx.api.navigation.navigate('/settings'); ``` **Navigation Routes**: The navigation API uses the same route structure as the main application. Common routes include `/accounts`, `/portfolio`, `/activities`, `/goals`, and `/settings`. --- ## Query API Access an addon-owned React Query client and ask the host to refresh selected caches. ### Methods #### `getClient(): QueryClient` Gets a QueryClient instance for the addon sandbox. This is not the raw host QueryClient. ```typescript const queryClient = ctx.api.query.getClient(); // Use standard React Query methods const accounts = await queryClient.fetchQuery({ queryKey: ['accounts'], queryFn: () => ctx.api.accounts.getAll(), }); ``` When the addon calls `invalidateQueries` or `refetchQueries` on this client with a string query key, Wealthfolio mirrors that request to the host cache. #### `invalidateQueries(queryKey: string | string[]): void` Invalidates queries to trigger refetch. ```typescript // Invalidate specific query ctx.api.query.invalidateQueries(['accounts']); // Invalidate multiple related queries ctx.api.query.invalidateQueries(['portfolio', 'holdings']); // Invalidate all account-related queries ctx.api.query.invalidateQueries(['accounts']); ``` #### `refetchQueries(queryKey: string | string[]): void` Triggers immediate refetch of queries. ```typescript // Refetch portfolio data ctx.api.query.refetchQueries(['portfolio']); // Refetch multiple queries ctx.api.query.refetchQueries(['accounts', 'holdings']); ``` ### Integration with Events Combine Query API with event listeners for reactive data updates: ```typescript export default function enable(ctx: AddonContext) { let unlistenPortfolio: (() => void) | undefined; let unlistenMarket: (() => void) | undefined; // Invalidate relevant queries when portfolio updates void ctx.api.events.portfolio .onUpdateComplete(() => { ctx.api.query.invalidateQueries(['portfolio', 'holdings', 'performance']); }) .then((unlisten) => { unlistenPortfolio = unlisten; }); // Invalidate market data queries when sync completes void ctx.api.events.market .onSyncComplete(() => { ctx.api.query.invalidateQueries(['quotes', 'assets']); }) .then((unlisten) => { unlistenMarket = unlisten; }); ctx.onDisable(() => { unlistenPortfolio?.(); unlistenMarket?.(); }); } ``` --- ## UI Integration APIs ### Sidebar API Prefer declaring sidebar entries in `manifest.json` under `contributes.links.sidebar` (see below) — the host renders them without booting your addon and they survive reloads. The runtime `addItem` API is still available for **dynamic** items an addon adds while running. #### Declarative (preferred) ```jsonc "contributes": { "routes": [{ "id": "my-addon", "path": "/addons/my-addon" }], "links": { "sidebar": [ { "id": "my-addon", "route": "my-addon", "label": "My Addon", "icon": "wallet", "order": 100 } ] } } ``` A **route** is a durable addon page (host-renderable before the addon boots — the lazy-activation surface); a **link** is a placement in a host slot (only `"sidebar"` is consumed today) that references a declared `route` id of the same addon. The runtime `router.add({ id })` **must** equal `contributes.routes[].id`. #### `addItem(item: SidebarItem): SidebarItemHandle` For dynamic, runtime-added entries: ```typescript const sidebarItem = ctx.sidebar.addItem({ id: 'my-addon', label: 'My Addon', route: '/addons/my-addon', icon: 'wallet', // Optional — an AddonIconName (see below) order: 100, // Lower numbers appear first }); // Remove when addon is disabled ctx.onDisable(() => { sidebarItem.remove(); }); ``` #### Sidebar icons The `icon` field is typed as `AddonIconName` from `@wealthfolio/addon-sdk`. Import that type for autocomplete and a compile-time error on any invalid name: ```typescript import type { AddonIconName } from '@wealthfolio/addon-sdk'; const icon: AddonIconName = 'wallet'; // ✓ // const icon: AddonIconName = 'spinner'; // ✗ type error ``` Icons come from a curated set of [Phosphor](https://phosphoricons.com) icons (duotone weight) that Wealthfolio bundles and renders — the sidebar is host UI, so an addon **names** an icon rather than shipping one. Matching is case- and separator-insensitive (`'ChartLine'`, `'chart-line'`, and `'chartline'` are equivalent), and an unknown or omitted name renders a neutral `caret-right` fallback. This applies only to the **sidebar** icon. Inside your own route/page you render with your own React, so you can use any icon there — Lucide, Phosphor, or your own SVGs. The 80 supported names, by group (preview every glyph at [phosphoricons.com](https://phosphoricons.com)): - **Money & finance** — `wallet`, `coins`, `dollar`, `dollar-circle`, `bank`, `credit-card`, `piggy-bank`, `receipt`, `invoice`, `hand-coins`, `vault`, `chart-line-up`, `chart-line`, `trend-up`, `trend-down`, `percent`, `scales`, `calculator` - **Charts & analytics** — `chart-bar`, `chart-pie`, `chart-pie-slice`, `chart-donut`, `gauge`, `target`, `presentation` - **Assets** — `house`, `buildings`, `car`, `airplane`, `bicycle`, `diamond`, `bitcoin`, `storefront`, `briefcase`, `package`, `cube` - **General** — `star`, `heart`, `gift`, `trophy`, `medal`, `lightning`, `sparkle`, `bell`, `tag`, `bookmark`, `flag`, `fire`, `rocket`, `lightbulb`, `graduation-cap`, `barbell`, `fork-knife`, `coffee`, `wine`, `shopping-cart`, `shopping-bag`, `basket` - **Time & place** — `calendar`, `calendar-dots`, `calendar-check`, `clock`, `hourglass`, `globe`, `map-pin`, `compass` - **Productivity** — `folder`, `files`, `notebook`, `clipboard-text`, `list-checks`, `sliders`, `wrench`, `toolbox`, `puzzle-piece`, `plugs-connected`, `app-window`, `squares-four`, `stack`, `kanban` ### Router API Register the renderer for your addon's pages. The host owns a single React root per addon and mounts the route's `component` itself, so hand it a component rather than managing a root by hand. #### `add(route: RouteConfig): void` ```typescript import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk'; import { MyPage } from './pages/MyPage'; let addonCtx: AddonContext | undefined; const MyRoute = () => ( ); const enable: AddonEnableFunction = (ctx) => { addonCtx = ctx; ctx.router.add({ id: 'my-addon', path: '/addons/my-addon', component: MyRoute }); ctx.onDisable(() => { addonCtx = undefined; }); }; export default enable; ``` Provide **exactly one** of `component` (preferred — the host manages mount/unmount in its single root) or `render` (a legacy imperative escape hatch given the container element); if both are set, `component` wins. Do **not** call `createRoot` yourself — a per-route root leaves an orphaned tree whose re-renders never reach the DOM. The component receives the current route as a `{ location }` prop (`AddonRouteLocation` with `pathname/search/hash/params`); the sandbox has **no** react-router provider, so `useLocation()` / `useParams()` are unavailable. When a route is also declared in `manifest.json` `contributes.routes`, the runtime `router.add({ id })` **must use the same `id`** — a mismatch renders a blank "route is not available" page. Runtime `router.add` still works for routes an addon adds dynamically while running. Addon routes must stay inside the addon's namespace. For an addon with id `my-addon`, use `/addons/my-addon` or `/addon/my-addon`. For ids ending in `-addon`, Wealthfolio also allows the stripped slug, for example `swingfolio-addon` can use `/addons/swingfolio`. --- ## Error Handling ### API Error Types ```typescript interface APIError { code: string; message: string; details?: any; } ``` Common error codes: - `PERMISSION_DENIED` - Insufficient permissions - `NOT_FOUND` - Resource not found - `VALIDATION_ERROR` - Invalid data provided - `NETWORK_ERROR` - Connection issues - `RATE_LIMITED` - Too many requests ### Best Practices ```typescript try { const accounts = await ctx.api.accounts.getAll(); } catch (error) { if (error.code === 'PERMISSION_DENIED') { ctx.api.logger.error('Missing account permissions'); // Show user-friendly message } else if (error.code === 'NETWORK_ERROR') { ctx.api.logger.warn('Network issue, retrying...'); // Implement retry logic } else { ctx.api.logger.error('Unexpected error:', error); // General error handling } } ``` --- ## Advanced Usage ### Batch Operations ```typescript // Efficient batch processing const activities = await Promise.all([ ctx.api.activities.getAll('account-1'), ctx.api.activities.getAll('account-2'), ctx.api.activities.getAll('account-3'), ]); // Batch create const newActivities = await ctx.api.activities.saveMany({ creates: [ { /* activity 1 */ }, { /* activity 2 */ }, { /* activity 3 */ }, ], }); ``` ### Real-time Updates ```typescript export default function enable(ctx: AddonContext) { const unsubscribers: Array<() => void> = []; // Listen for multiple events void Promise.all([ ctx.api.events.portfolio.onUpdateComplete(() => refreshData()), ctx.api.events.market.onSyncComplete(() => updatePrices()), ctx.api.events.import.onDrop((event) => handleImport(event)), ]).then((listeners) => { unsubscribers.push(...listeners); }); // Clean up all listeners ctx.onDisable(() => { unsubscribers.forEach((unsub) => unsub()); }); } ``` ### Caching Strategies ```typescript // Simple in-memory cache const cache = new Map(); let unlistenPortfolio: (() => void) | undefined; void ctx.api.events.portfolio .onUpdateComplete(() => { cache.delete('accounts'); }) .then((unlisten) => { unlistenPortfolio = unlisten; }); async function getCachedAccounts() { if (cache.has('accounts')) { return cache.get('accounts'); } const accounts = await ctx.api.accounts.getAll(); cache.set('accounts', accounts); return accounts; } ctx.onDisable(() => { unlistenPortfolio?.(); cache.clear(); }); ``` --- ## TypeScript Support Full TypeScript definitions are provided for all APIs: ```typescript import type { AddonContext, Account, Activity, Holding, PerformanceHistory, PerformanceSummary, // ... and many more } from '@wealthfolio/addon-sdk'; // Type-safe API usage const accounts: Account[] = await ctx.api.accounts.getAll(); const holdings: Holding[] = await ctx.api.portfolio.getHoldings(accounts[0].id); ``` ## Performance Tips 1. **Use batch operations** when possible 2. **Implement caching** for expensive operations 3. **Listen to relevant events only** 4. **Clean up resources** in disable function 5. **Use React.memo** for expensive components 6. **Debounce user inputs** for search/filter --- **Ready to build?** Check out the [Getting Started guide](/docs/addons/getting-started) to see these APIs in action!
--- # Getting Started Source: https://wealthfolio.app/docs/addons/getting-started
## Prerequisites ```bash # Check Node.js version (requires 20.19+ or 22.12+) node --version # Check pnpm pnpm --version # Install pnpm if needed npm install -g pnpm ``` Requirements: - Node.js 20.19+ or 22.12+ and pnpm - Wealthfolio desktop app (optional but recommended: running in development mode for live reload and testing) - Basic TypeScript and React knowledge - Code editor (VS Code recommended) ## Start Wealthfolio (Recommended) For the best development experience with live addon reloading, start Wealthfolio in addon development mode: ```bash # Clone Wealthfolio repository (if not already done) git clone https://github.com/wealthfolio/wealthfolio.git cd wealthfolio # Install dependencies pnpm install # Start in addon development mode (enables live addon reloading) VITE_ENABLE_ADDON_DEV_MODE=true pnpm tauri dev ``` This enables: - Live addon reload when files change - Better error messages and debugging - Automatic addon discovery - Console logging for development ## Create New Addon ```bash # Navigate to development directory cd ~/Documents/WealthfolioAddons # Create addon using CLI npx @wealthfolio/addon-dev-tools create # Navigate and install cd pnpm install ``` This will scaffold a new addon project with the following structure: ``` hello-world-addon/ ├── src/ │ ├── addon.tsx # Main addon entry point │ ├── components/ # React components │ ├── hooks/ # React hooks │ ├── pages/ # Addon pages │ ├── utils/ # Utility functions │ └── types/ # Type definitions ├── dist/ # Built files (generated) ├── manifest.json # Addon metadata and permissions ├── package.json # NPM package configuration ├── vite.config.ts # Build configuration ├── tsconfig.json # TypeScript configuration └── README.md # Documentation ``` ## Manifest File `manifest.json` defines metadata and permissions: ```json { "id": "hello-world-addon", "name": "Hello World Addon", "version": "1.0.0", "description": "My first Wealthfolio addon", "author": "Your Name", "main": "dist/addon.js", "sdkVersion": "3.6.1", "minWealthfolioVersion": "3.6.1", "enabled": true, "contributes": { "routes": [{ "id": "hello-world", "path": "/addons/hello-world" }], "links": { "sidebar": [ { "id": "hello-world", "route": "hello-world", "label": "Hello World", "icon": "puzzle-piece", "order": 100 } ] } }, "permissions": [], "hostDependencies": { "@wealthfolio/addon-sdk": "^3.6.1", "@wealthfolio/ui": "^3.6.0", "react": "^19.2.0", "react-dom": "^19.2.0" } } ``` Navigation is **declarative**. A `contributes.routes` entry is a durable addon page — the host can render it (and build the sidebar) before your addon boots, so nothing runs until the route is first visited. A `contributes.links` entry places that route in a host slot (only `"sidebar"` is consumed today) and references a declared route `id`. The runtime `router.add({ id })` you register in `addon.tsx` **must use the same `id`** as its declared route. Baseline capabilities — `ui`, `query`, `toast`, `logger`, and `storage` — are implicit and never declared in `permissions`. Only data domains and `files`, `network`, `secrets`, `events`, `snapshots`, and `settings` need an entry. ## Main Addon File `src/addon.tsx` contains the addon logic: ```typescript import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk'; function HelloWorldPage() { return (

Hello Wealthfolio

Your first addon is working.

Success

You've successfully created and loaded your first addon.

); } // The host owns a single React root per addon and mounts the route `component` // itself, with no access to the addon context. Capture it at enable time so the // route wrapper can hand it down. (Do NOT call createRoot yourself — the host // manages the lifecycle.) let addonCtx: AddonContext | undefined; const HelloWorldRoute = () => ( ); const enable: AddonEnableFunction = (ctx) => { addonCtx = ctx; // The sidebar item + route are declared in manifest.json (`contributes`), so // the host renders navigation without booting the addon. The route `id` MUST // match `contributes.routes[].id`. ctx.router.add({ id: 'hello-world', path: '/addons/hello-world', component: HelloWorldRoute, }); ctx.api.logger.info('Hello World addon loaded'); // The host owns the React root, so there is nothing to unmount here. ctx.onDisable(() => { addonCtx = undefined; ctx.api.logger.info('Hello World addon disabled'); }); }; export default enable; ``` Hand the host a `component` and let it own the single React root — do **not** call `createRoot` yourself (a per-route root leaves an orphaned tree whose re-renders never reach the DOM). The component receives the current route as a `{ location }` prop; the sandbox has no react-router provider, so `useLocation()` / `useParams()` are unavailable. `render` remains as a legacy imperative escape hatch, but `component` is preferred. The sidebar `icon` (declared in `contributes.links`) is one of a curated set of [Phosphor](https://phosphoricons.com) names, typed as `AddonIconName`. See the full list in the [API reference](/docs/addons/api-reference#sidebar-icons). ## Start Development ```bash # Start development server (recommended) pnpm dev:server ``` Output: ``` Wealthfolio Addon Development Server Addon: hello-world-addon Server: http://localhost:3001 Watching for changes... ``` ### Hot Reload Features - File watching in `src/` directory - Fast rebuilds with Vite - Hot Module Replacement for component updates - Auto-discovery by Wealthfolio - Error recovery with overlay messages ### Available Commands ```bash pnpm dev:server # Start development server (recommended) pnpm build # Production build pnpm type-check # Run TypeScript checks pnpm lint # Run ESLint pnpm format # Run Prettier pnpm bundle # Bundle addon for distribution ``` Verify in Wealthfolio: 1. Open Wealthfolio (preferably in development mode with `pnpm tauri dev`) 2. Check sidebar for "Hello World" 3. Click to load addon page 4. Check console for log message ## Add Data Access For data access, it's recommended to use [TanStack Query](https://tanstack.com/query/latest). First, install TanStack Query in your addon: ```bash pnpm add @tanstack/react-query@^5.62.7 ``` Update `src/addon.tsx` to access portfolio data using TanStack Query: ```typescript import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; import type { AddonContext, AddonEnableFunction, Account } from '@wealthfolio/addon-sdk'; function HelloWorldPage({ ctx }: { ctx: AddonContext }) { const { data: accounts = [], isLoading, isError, error, refetch, } = useQuery({ queryKey: ['accounts'], queryFn: () => ctx.api.accounts.getAll(), staleTime: 5 * 60 * 1000, // 5 minutes refetchOnWindowFocus: false, }); return (

Hello Wealthfolio

Portfolio Summary

{isLoading ? (
Loading accounts...
) : isError ? (

Failed to load accounts: {error?.message}

) : (

You have {accounts.length} account{accounts.length !== 1 ? 's' : ''}:

{accounts.length > 0 ? (
{accounts.map((account) => (

{account.name}

{account.currency} • {account.isActive ? 'Active' : 'Inactive'}

{account.totalValue?.toLocaleString() || 'N/A'}
Total Value
))}
) : (

No accounts found. Add an account in Wealthfolio to see data.

)}
)}
); } // Capture the context at enable time so the route wrapper can supply it (and a // shared QueryClientProvider) to the page. The QueryClientProvider shares one // cache across route navigations. let addonCtx: AddonContext | undefined; const HelloWorldRoute = () => ( ); const enable: AddonEnableFunction = (ctx) => { addonCtx = ctx; ctx.router.add({ id: 'hello-world', path: '/addons/hello-world', component: HelloWorldRoute, }); ctx.onDisable(() => { addonCtx = undefined; }); }; export default enable; ``` ## Update Permissions Update `manifest.json` to include account access: ```json { "id": "hello-world-addon", "name": "Hello World Addon", "version": "1.0.0", "description": "My first Wealthfolio addon", "author": "Your Name", "main": "dist/addon.js", "sdkVersion": "3.6.1", "minWealthfolioVersion": "3.6.1", "enabled": true, "contributes": { "routes": [{ "id": "hello-world", "path": "/addons/hello-world" }], "links": { "sidebar": [ { "id": "hello-world", "route": "hello-world", "label": "Hello World", "icon": "puzzle-piece", "order": 100 } ] } }, "permissions": [ { "category": "accounts", "functions": ["getAll"], "purpose": "Display account summary" } ], "hostDependencies": { "@tanstack/react-query": "^5.90.0", "@wealthfolio/addon-sdk": "^3.6.1", "@wealthfolio/ui": "^3.6.0", "react": "^19.2.0", "react-dom": "^19.2.0" } } ``` Only the `accounts` data domain needs declaring — navigation now lives in `contributes`, and the `ui` capability it used to require is baseline (implicit). ## Build and Package ```bash # Build for production pnpm build # Package for distribution pnpm bundle ``` Creates `dist/hello-world-addon.zip` for installation. Source maps are useful during development, but they are not required in install bundles. Exclude `*.map` files from release ZIPs to keep packages small. ## Debugging and Development Tools ### Browser Developer Tools Access full debugging capabilities: ```typescript // Use console for debugging ctx.api.logger.info('Debug message'); ctx.api.logger.error('Error message'); // Access React DevTools // Components will show up in React DevTools extension ``` ### Error Handling ```typescript export default function enable(ctx: AddonContext) { try { // Your addon code } catch (error) { ctx.api.logger.error('Addon error:', error); // Handle gracefully } } ``` ### Development Server Features - Port: `http://localhost:3001` - CORS configured for Wealthfolio - Source maps for debugging - Real-time TypeScript checking - Hot Module Replacement ## IDE Setup ### VS Code (Recommended) Recommended extensions: - TypeScript and JavaScript Language Features - ES7+ React/Redux/React-Native snippets - Tailwind CSS IntelliSense - Auto Rename Tag - Error Lens Create `.vscode/settings.json`: ```json { "typescript.preferences.importModuleSpecifier": "relative", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" } ``` ## Code Quality and Testing ### Manual Testing 1. Start development server 2. Open Wealthfolio 3. Navigate to your addon 4. Test all features 5. Check console for errors ### Code Quality Commands ```bash # Type checking pnpm type-check # Linting pnpm lint # Formatting pnpm format ``` ## Configuration Files ### Package.json Scripts ```json { "scripts": { "dev:server": "wealthfolio dev", "build": "vite build", "type-check": "tsc --noEmit", "lint": "eslint src --ext .ts,.tsx", "format": "prettier --write \"src/**/*.{ts,tsx}\"", "bundle": "pnpm build && zip -r addon.zip manifest.json dist/ -x \"*.map\"" } } ``` ### TypeScript Configuration ```json { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ``` ### Vite Build Configuration ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { lib: { entry: 'src/addon.tsx', formats: ['es'], fileName: () => 'addon.js', }, rollupOptions: { external: [ 'react', 'react-dom', 'react-dom/client', '@wealthfolio/addon-sdk', '@wealthfolio/ui', '@tanstack/react-query', ], }, }, }); ``` ## Next Steps You now understand: - Project structure and development workflow - Permission system and security model - Hot reload development - API integration for portfolio data - UI integration with navigation Continue with: - [API Reference](/docs/addons/api-reference) - All available APIs - [Examples](https://github.com/wealthfolio/wealthfolio-addons/tree/main/official) - Real addon implementations
--- # Core Concepts Source: https://wealthfolio.app/docs/concepts
## Account | Field | Purpose | |-------|---------| | **Name** | Display label | | **Group** | Collapses similar accounts (e.g. *RRSP*) | | **Type** | *Securities · Cash · Crypto* | | **Currency** | FX source for the account | | **Default? / Active?** | Workflow helpers | ## Activity All portfolio changes resolve into one of these actions. | Type | Cash Δ | Holdings Δ | Notes | |------|--------|------------|-------| | **BUY** | − | + | Fee optional | | **SELL** | + | − | Gain tracked | | **DIVIDEND** | + | 0 | Cash income | | **INTEREST** | + | 0 | Cash income | | **DEPOSIT / WITHDRAWAL** | ± | 0 | Cash only | | **TRANSFER_IN / OUT** | ± | ± | Internal keeps cost basis; External seeds/removes positions (opening balances, gifts, write-offs) | | **FEE / TAX** | − | 0 | Stand‑alone charges | | **SPLIT** | 0 | qty/price adjust | No value change | Cash legs post to the synthetic symbol `$CASH‑` automatically. ## Symbol lookup 1. Exact ticker (`AAPL`, `BTC‑USD`). 2. Ticker + suffix (`RY.TO`). 3. First Yahoo Finance hit. Need to override? Use the **Universal Symbol ID**. ## FX rates - Pulled every 6 h from the default market data provider. - View/edit: `Settings → General → Exchange Rates`. - Manual rates override auto values until you change them again.
--- # Activity Types Source: https://wealthfolio.app/docs/concepts/activity-types Activities are the atomic events that drive portfolio state in Wealthfolio—every trade, cash movement, fee, or corporate action is recorded as an **activity**. Accurate performance, cash-flow, and tax reporting all start with choosing the right activity type. --- ## 1 · Activity Types at a Glance | Type | Typical Use Case | Cash Impact | Holdings Impact | | ------------------ | ------------------------------------------------ | -------------------- | --------------------------- | | **BUY** | Buy a security, buy to open, or buy to cover. | ↓ cash | ↑ long or ↓ short | | **SELL** | Sell a holding, sell to close, or sell short. | ↑ cash | ↓ long or ↑ short | | **SPLIT** | Stock split or reverse split. | — | qty & unit cost adjusted | | **DIVIDEND** | Cash dividend on a security. | ↑ cash | — | | **INTEREST** | Interest on cash or fixed-income. | ↑ cash | — | | **CREDIT** | Cash credit (bonus, rebate, refund). | ↑ cash | — | | **DEPOSIT** | Funds added from outside Wealthfolio. | ↑ cash | — | | **WITHDRAWAL** | Funds sent to an external account. | ↓ cash | — | | **TRANSFER_IN** | Move cash or securities **into** this account. With the **External** flag, also covers opening balances, gifts, and inheritances. | ↑ cash or ↑ quantity | ↑ quantity (for securities) | | **TRANSFER_OUT** | Move cash or securities **out of** this account. With the **External** flag, also covers write-offs and crypto sent to untracked wallets. | ↓ cash or ↓ quantity | ↓ quantity (for securities) | | **FEE** | Standalone brokerage or platform fee. | ↓ cash | — | | **TAX** | Tax deducted from the account. | ↓ cash | — | | **ADJUSTMENT** | Non-trade correction (see subtypes). | Depends on subtype | Depends on subtype | --- ## 2 · Activity Types in Detail ### Trading Activities | Type | What It Does | Cash Impact | Holdings Impact | | --------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ----------------------------------------------------------------------------------- | | **BUY** | Purchase a security. With Close Position intent, buys back a short stock/ETF lot or short option contract. | Cash decreases by total cost (qty x price + fees) | Opens/increases a long lot, or reduces a short lot | | **SELL** | Sell a security you hold. With Open Position intent, opens/increases a short stock/ETF lot or written option. | Cash increases by net proceeds (qty x price - fees) | Closes long lots FIFO, or opens/increases a signed short lot when explicitly marked | | **SPLIT** | Record a stock split or reverse split. Adjusts share count and per-share cost so total value stays the same. | No change | Quantity and unit cost adjusted; total cost basis unchanged | ### Income Activities | Type | What It Does | Cash Impact | Holdings Impact | | ------------ | ------------------------------------------------------------------------- | --------------------------------- | --------------- | | **DIVIDEND** | Cash dividend paid on a security you hold. | Cash increases by dividend amount | No change | | **INTEREST** | Interest earned on cash balances or fixed-income holdings. | Cash increases by interest amount | No change | | **CREDIT** | A cash credit applied to your account (see subtypes below for specifics). | Cash increases by credit amount | No change | For a bond coupon, add an **INTEREST** activity and select the bond in the optional **Symbol** field. Wealthfolio attributes the payment to that bond and includes it in the bond's income and total return. Coupon payments currently remain in the general Interest category; dedicated coupon-specific UI and reporting are not yet available. ### Cash Flow Activities | Type | What It Does | Cash Impact | Holdings Impact | | -------------- | -------------------------------------------------------------------------------------------- | -------------- | --------------- | | **DEPOSIT** | Money you add to your brokerage account from an external source (bank transfer, wire, etc.). | Cash increases | No change | | **WITHDRAWAL** | Money you take out of your brokerage account to an external destination. | Cash decreases | No change | ### Transfer Activities Transfers come in two flavors, controlled by an **External** flag on the activity: - **Internal** (External unchecked): paired with a matching activity on another Wealthfolio account. Cost basis travels between accounts. No new capital enters or leaves your tracked portfolio. - **External** (External checked): one-sided. The other side of the move lives outside Wealthfolio. Used for opening balances, gifts/inheritances received, RSUs vested, write-offs, crypto sent to untracked wallets, etc. | Type | What It Does | Cash Impact | Holdings Impact | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------- | | **TRANSFER_IN** | Move cash or securities **into** this account. Internal: paired with `TRANSFER_OUT` from another Wealthfolio account, cost basis preserved. External: opens a new lot at the cost basis you provide. | Cash or position increases | Position quantity increases (for securities) | | **TRANSFER_OUT** | Move cash or securities **out of** this account. Internal: cost basis exported to the destination Wealthfolio account. External: lots closed via FIFO with no gain realised (the asset leaves your portfolio). | Cash or position decreases | Position quantity decreases (for securities) | ### Expense Activities | Type | What It Does | Cash Impact | Holdings Impact | | ------- | --------------------------------------------------------------------------------------------------------------------------- | -------------- | --------------- | | **FEE** | A stand-alone brokerage or platform fee not tied to a specific trade (e.g., annual account fee, custody fee, advisory fee). | Cash decreases | No change | | **TAX** | A tax charge deducted from your account (e.g., dividend withholding tax, capital gains tax). | Cash decreases | No change | ### Position Adjustment Activities | Type | What It Does | Cash Impact | Holdings Impact | | -------------- | ------------------------------------------------------------- | ------------------ | ------------------ | | **ADJUSTMENT** | A non-trade correction or transformation (see subtypes below). | Depends on subtype | Depends on subtype | --- ## 3 · Subtypes Some activity types have **subtypes** that provide more specific behavior. When you select a subtype, Wealthfolio automatically handles the underlying mechanics for you. ### Trade Intent Subtypes `BUY` and `SELL` can carry position intent. This is required for options and used for explicit stock/ETF short workflows. | Subtype | Parent Type | What It Does | Common labels / aliases | | ------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | | **POSITION_OPEN** | BUY / SELL | Opens or extends a position. For `BUY`, this is a long/opening buy. For `SELL`, this is a sell-to-open or explicit stock/ETF short. | Buy to Open, BTO, Sell to Open, STO, Sell Short, Short Sell | | **POSITION_CLOSE** | BUY / SELL | Closes or reduces an existing position. For `BUY`, this covers a short. For `SELL`, this closes a long option position. | Buy to Close, BTC, Buy to Cover, Cover Short, Sell to Close, STC | Plain stock/ETF `SELL` activities do **not** open short lots. Use **Sell Short** to open or increase a short stock/ETF position, and **Buy to Cover** to reduce it. ### Dividend Subtypes | Subtype | Parent Type | What It Does | Example | | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | **DRIP** (Dividend Reinvestment) | DIVIDEND | Dividend is automatically reinvested to buy more shares of the same security. Wealthfolio records both the dividend income and the resulting purchase. | You own 100 shares of AAPL. A $50 dividend is paid and automatically used to buy 0.25 more shares. | | **Dividend in Kind** | DIVIDEND | Dividend paid as additional shares of the same security rather than cash. Wealthfolio records both the dividend income and resulting acquisition. | A fund pays a stock dividend and you receive additional units instead of cash. | ### Interest Subtypes | Subtype | Parent Type | What It Does | Example | | ------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | **Staking Reward** | INTEREST | Crypto staking income received as additional tokens. Wealthfolio records the interest income and the resulting token acquisition. | You stake 10 ETH and receive 0.05 ETH as a staking reward. | ### Credit Subtypes | Subtype | Parent Type | What It Does | Counts as New Capital? | Example | | ------------------ | ----------- | ---------------------------------------------------------------- | ---------------------- | ------------------------------------------------------ | | **Bonus** | CREDIT | An external cash credit like a sign-up bonus or referral reward. | Yes (like a deposit) | Your broker gives you a $100 welcome bonus. | | **Trading Rebate** | CREDIT | A rebate on trading costs (e.g., maker rebate, volume discount). | No (reduces costs) | You receive a $5 maker rebate for providing liquidity. | | **Fee Refund** | CREDIT | A reversal or correction of a previously charged fee. | No (reverses a cost) | Your broker refunds an erroneous $25 service charge. | ### Adjustment Subtypes | Subtype | Parent Type | What It Does | Example | | ----------------- | ----------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Option Expiry** | ADJUSTMENT | Removes a worthless expired option contract from your holdings. No cash changes hands. | Your call option on TSLA expires out of the money. The position is removed and the premium paid is realized as a loss. | --- ## 4 · Quick-Start Cheat-Sheet | Scenario | Recommended Activities | Why | | ---------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | | **Setting up for the first time** | External `TRANSFER_IN` for each position + `DEPOSIT` for cash | Fastest way to seed your current portfolio snapshot | | **Day-to-day trading** | `BUY`, `SELL`, `DIVIDEND`, `INTEREST` | Full profit/loss tracking and cash reconciliation | | **Moving between accounts** | `TRANSFER_OUT` from source + `TRANSFER_IN` to destination | Preserves cost basis; avoids phantom gains or losses | | **Standalone charges** | `FEE` for account fees, `TAX` for tax deductions | Keeps expenses explicit and separate from trades | | **Received a gift or inheritance** | External `TRANSFER_IN` | Records the position without implying a purchase | | **Writing off a worthless stock** | External `TRANSFER_OUT` | Removes the position without needing sale proceeds | | **Stock split (e.g., 4-for-1)** | `SPLIT` | Adjusts quantity and per-share cost; total value unchanged | | **Dividend reinvestment (DRIP)** | `DIVIDEND` with DRIP subtype | Automatically records both the dividend and the share purchase | | **Crypto staking rewards** | `INTEREST` with Staking Reward subtype | Records token income and acquisition in one step | | **Sell short stock/ETF** | `SELL` with Position Open / Sell Short intent | Creates a signed short lot; plain sells never open shorts | | **Buy to cover stock/ETF short** | `BUY` with Position Close / Buy to Cover intent | Reduces the oldest short lot first | | **Buy or sell options** | `BUY` / `SELL` with Open or Close intent | Distinguishes BTO/STO/BTC/STC and applies the option multiplier | | **Broker sign-up bonus** | `CREDIT` with Bonus subtype | Tracks the bonus as new capital entering your portfolio | | **Option expired worthless** | `ADJUSTMENT` with Option Expiry subtype | Cleanly removes the position and realizes the loss | | **Fee refund from broker** | `CREDIT` with Fee Refund subtype | Reverses the fee without inflating your capital contributions | --- ## 5 · Workflow Styles ### Simple (Holdings-Only) - Use External `TRANSFER_IN` / `TRANSFER_OUT` to set up your current positions. - Adjust cash once with `DEPOSIT` / `WITHDRAWAL`. - **Good for:** Quick onboarding, backfilling missing history, or when you only care about tracking portfolio value over time. ### Full (Transaction-Level) 1. Seed each account with a `DEPOSIT`. 2. Record every `BUY`, `SELL`, `DIVIDEND`, `INTEREST`. 3. Mirror transfers between accounts with `TRANSFER_IN` / `TRANSFER_OUT`. 4. Log ad-hoc expenses via `FEE` and `TAX`. - **Good for:** Precise time-weighted and money-weighted returns, cash-flow analysis, and tax reporting. You can freely mix the two styles — for example, backfill long-held shares with an External `TRANSFER_IN`, then switch to `BUY`/`SELL` going forward. --- ## 6 · How Fees Work Fees can be recorded in two ways, depending on the situation: | Method | When to Use | How It Works | | --------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Inline fee on a trade** | The fee is part of a BUY or SELL order | Add the fee amount directly on the BUY/SELL activity. For buys, the fee increases your cost basis. For sells, the fee reduces your proceeds. | | **Standalone FEE activity** | The fee is not tied to a specific trade (account fee, advisory fee, etc.) | Create a separate `FEE` activity. Cash is reduced by the fee amount. | **Never double-count fees.** If your broker statement shows a $10 commission on a BUY order, either include it as the fee on the BUY activity *or* create a separate FEE activity — not both. --- ## 7 · How Transfers Work Transfers come in two flavors: ### Cash Transfers Record a `TRANSFER_OUT` on the source account and a `TRANSFER_IN` on the destination account for the same amount. No security/symbol is needed. ### Securities Transfers When you transfer a stock or other holding between accounts, Wealthfolio preserves the original cost basis: 1. `TRANSFER_OUT` on the source account (specify the security and quantity) 2. `TRANSFER_IN` on the destination account (same security and quantity) The receiving account inherits the original purchase cost, so your gain/loss calculations remain accurate. **Tip:** If the transfer is from an external source (not another Wealthfolio account), a `TRANSFER_IN` by itself is fine — you don't need a matching `TRANSFER_OUT`. --- ## 8 · Key Rules & Gotchas | Topic | What to Know | Why It Matters | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | **Lot selection** | FIFO (First In, First Out) is the default method. When you sell, the oldest shares are sold first. | Affects your realized profit/loss and tax calculations. | | **Fractional shares** | Quantities support up to eight decimal places. | You can accurately track fractional share purchases and crypto holdings. | | **Retroactive entries** | Activities can be inserted for any past date — Wealthfolio recalculates all balances automatically. | You can backfill historical trades at any time without breaking anything. | | **Multi-currency** | Currency conversion uses the exchange rate on the trade date. | Keeps cross-currency returns and cash balances matching your broker statement. | | **Options multiplier** | For options, the price is per-share but each contract covers multiple shares (typically 100). The multiplier is applied automatically. | Ensures correct cost basis for option positions. | | **Options intent** | Options need explicit Open or Close intent: BTO, STO, BTC, or STC. | Prevents an option close from being interpreted as a new position. | | **Short positions** | Plain stock/ETF sells do not open shorts. Use Sell Short, then Buy to Cover. | Prevents accidental negative positions from oversells. | | **Inline fees vs. standalone** | `BUY`/`SELL` can include an inline fee, or you can log a separate `FEE`. Never do both for the same charge. | Avoids double-counting expenses. | | **CSV import format** | CSV files must be UTF-8 encoded with ISO-8601 dates (`2025-03-15`), decimal points (not commas), and headers matching the expected column names. | Prevents import errors. | --- **Next step:** Import or create activities manually or via CSV uploader, then view the timeline in the **Activities** tab to verify that cash and positions reconcile as expected. --- # Cost Basis & Lots Source: https://wealthfolio.app/docs/concepts/cost-basis-and-lots Every share you buy creates a **long lot**; every explicit Sell Short creates a signed **short lot**. A lot is a unit of cost basis that follows that position until it's closed. Lot accounting is what makes the difference between "you sold some Apple, here's your gain" and "you sold these specific shares bought on this date at this price, here's your gain and your remaining basis." This page covers how Wealthfolio's lot tracking works, why we made the choices we did, and what to expect when activities touch your lots. --- ## 1 · The mental model Think of each `BUY` as opening a numbered lot: ``` Lot #1: 2024-01-10 20 shares @ $150.00 cost basis = $3,000 Lot #2: 2024-04-15 10 shares @ $180.00 cost basis = $1,800 Lot #3: 2024-08-22 5 shares @ $200.00 cost basis = $1,000 ----------- Total: 35 shares $5,800 Average cost: $165.71 ``` The **average cost** line is a convenience display — total cost basis of your open lots divided by the shares you hold. It is *not* a moving-average accounting method: Wealthfolio doesn't keep a running moving-average price, and the figure simply re-derives from whatever lots are still open. (See [the note for moving-average jurisdictions](#9--why-not-lifo-weighted-average-or-specific-identification) below.) Lots only exist when you track real activities. A **manually-entered holding** (a manual account snapshot) stores just a quantity and the average cost you type in — there's no per-lot history behind it, so FIFO and realized-gain tracking only kick in once the position is built from actual `BUY` / `SELL` / `TRANSFER` activities. Every `SELL`, **Buy to Cover**, or `TRANSFER_OUT` closes shares from one or more lots. Wealthfolio decides **which** lots to close using a method called **FIFO**. --- ## 2 · FIFO: first in, first out Wealthfolio closes the **oldest lot first**. Sell 25 shares from the position above and you'll close all of Lot #1 (20 shares) plus 5 shares from Lot #2: ``` Sell 25 shares @ $195 Closed: Lot #1: 20 shares × ($195 - $150) = $900 realized gain Lot #2: 5 shares × ($195 - $180) = $75 realized gain Realized gain total: $975 Remaining: Lot #2: 5 shares @ $180 = $900 cost basis Lot #3: 5 shares @ $200 = $1,000 cost basis Total: 10 shares $1,900 ``` Why FIFO? - **Defensible.** It's the default cost-basis method in most jurisdictions, so your Wealthfolio numbers will tend to match what your broker reports for tax purposes. - **Simple to reason about.** No "which lots am I selling?" guessing. The oldest go first, period. - **Stable.** Adding a backdated trade doesn't reshuffle which shares were "sold" in later sales unless the backdate is actually earlier than those sales. **Selling more than you hold?** A plain `SELL` still does not open or increase a short lot. Wealthfolio closes every available long lot, caps the position at zero, and flags the oversell. If you meant to open a short stock or ETF position, use **Sell Short** instead; that creates a signed negative lot. Use **Buy to Cover** to close short lots FIFO. --- ## 3 · How activities change lots | Activity | Effect on lots | | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BUY` | Opens a new long lot at the trade price. Fee is folded into cost basis. | | `SELL` | Closes shares from the oldest long lot(s) first. Fee reduces proceeds. Plain sells do not open short lots. | | `SELL` (Sell Short) | Opens or extends a signed short lot for stocks and ETFs. | | `BUY` (Buy to Cover) | Closes shares from the oldest short lot(s) first. | | `SPLIT` | Adjusts quantity and per-share cost on every existing lot. Total cost basis is unchanged. | | `DIVIDEND` (DRIP subtype) | Opens a new lot at the DRIP price for the reinvested shares. | | `DIVIDEND in Kind` | Opens a new lot for additional units of the same security at the broker-reported cost basis. | | `TRANSFER_IN` | Restores the original lots (cost basis preserved) when paired with a `TRANSFER_OUT` from another Wealthfolio account. With the **External** flag, opens a single lot at the cost basis you provide. | | `TRANSFER_OUT` | Closes lots oldest-first; no gain is realised. Internal: basis travels to the destination account. External: basis simply leaves Wealthfolio with the asset. | | `ADJUSTMENT` (Option Expiry) | Closes the option lot to zero, realizing the premium paid as a loss. | | `DEPOSIT` / `WITHDRAWAL` / `INTEREST` / `FEE` / `TAX` / cash `DIVIDEND` | No effect on lots. These move your cash balance only — cost basis lives entirely in share lots. | --- ## 4 · Worked example: BUY → SPLIT → SELL You buy, the stock splits, you sell. What does each activity do to your basis? ``` 2024-01-10 BUY 10 @ $400 Lot #1: 10 shares, cost $400/share, total cost $4,000 2024-06-01 SPLIT 4-for-1 Lot #1: 40 shares, cost $100/share, total cost $4,000 2024-09-15 SELL 25 @ $120 Closes 25 shares from Lot #1 Cost basis closed: 25 × $100 = $2,500 Proceeds: 25 × $120 = $3,000 Realized gain: $500 Remaining Lot #1: 15 shares, cost $100/share, total cost $1,500 ``` Notice that the SPLIT didn't realize a gain; it just rebalanced the lot. The realized gain on the SELL uses the **post-split** per-share cost. --- ## 5 · Transfers preserve cost basis When you move shares between two Wealthfolio accounts: ``` Account A (Brokerage) Account B (Roth IRA — *don't actually do this*) Lot: 100 shares @ $150 — ↓ TRANSFER_OUT 50 shares Lot: 50 shares @ $150 Lot: 50 shares @ $150 (received via TRANSFER_IN) ``` Both halves of the lot share the original cost basis. Wealthfolio doesn't realize a gain; moving shares isn't selling them. For transfers from **external** accounts (not another Wealthfolio account), a standalone `TRANSFER_IN` opens a single new lot at whatever cost basis you provide. Set it to your broker's "transferred-in basis" if you have it, or to the average cost as of the transfer date. --- ## 6 · DRIP and dividend-in-kind A `DIVIDEND` with the **DRIP** subtype does two things in one activity: 1. Records the dividend income at the cash amount. 2. Opens a new lot for the reinvested shares at the DRIP price. That new lot starts the FIFO clock on those shares from the DRIP date. Relevant for short-vs-long-term capital gains tax in some jurisdictions. A `DIVIDEND in Kind` opens a lot for additional units of the same security at the cost basis your broker reports. For a true spin-off into a different ticker, record the received security as an External `TRANSFER_IN` using the broker-reported basis. --- ## 7 · Fractional shares Lots are tracked to 8 decimal places. DRIP fractions, crypto satoshis, and broker fractional-share programs are all first-class. --- ## 8 · Cost basis and currency Each lot's cost basis is stored in the **asset's own currency**. A lot of a USD-listed stock carries a USD cost basis; a EUR-listed stock carries EUR — regardless of your account's base currency. If you enter a trade in a different currency than the asset, Wealthfolio converts it at the trade's FX rate and remembers the rate it used. What matters for tax: when you **sell**, the realized gain reported in your base currency is built from two different FX rates — - **Cost basis** is converted at the **acquisition-date** rate. - **Proceeds** are converted at the **disposal-date** rate. ``` Buy 10 shares @ $100 when 1 EUR = 1.10 USD → basis €909 Sell 10 shares @ $100 when 1 EUR = 1.00 USD → proceeds €1,000 Realized gain (EUR): €91 — even though the price in USD never moved. ``` In other words, **currency movement is folded into your realized gain**, not broken out separately. The share price can be flat in its local currency and you'll still show a gain or loss in your base currency from the FX swing. This is the standard treatment in most jurisdictions, but it's worth knowing before you reconcile against a broker statement denominated in the asset's currency. --- ## 9 · Why not LIFO, weighted-average, or specific-identification? FIFO is opinionated by design — but it's the starting point, not the end state. **Additional cost-basis methods will be added in future versions.** The most-requested alternatives on the roadmap: - **LIFO (last in, first out):** common in certain US tax strategies. Not implemented yet; planned for a future version. - **Weighted-average / moving-average cost (WAC — called ACB in Canada, _gleitender Durchschnittspreis_ in Austria/Germany):** several jurisdictions require a moving average rather than FIFO. Canada uses ACB by default; Austria taxes realized gains on a moving average. The "average cost" shown on a holding is _not_ this method — it's a display figure derived from your open FIFO lots, so after a partial sale it reflects the remaining lots rather than a running moving average. Until WAC ships, your Wealthfolio realized-gain numbers will diverge from a moving-average tax calculation, so you'll need to reconcile manually in the meantime. - **Specific identification:** picking which lots to close on each sale. Powerful for tax-loss harvesting, but adds a lot of UI complexity. Planned. The honest answer: we'd rather ship FIFO well first and add the other methods in later versions than ship three half-finished accounting modes at once. Vote on what should come next in [GitHub Discussions](https://github.com/wealthfolio/wealthfolio/discussions). --- ## 10 · Where to see your lots Open any holding from the **Holdings** page. The asset detail view has a **Lots** tab that shows every open lot, its purchase date, original cost, and current unrealized gain/loss. The same view powers the **realized vs. unrealized** breakdown on the Performance page. --- **Next step:** [Performance Metrics](/docs/concepts/performance-metrics) walks through how realized and unrealized gains combine into the TWR and MWR numbers on your dashboard. --- # Market Data & FX Source: https://wealthfolio.app/docs/concepts/market-data-and-fx ### How symbols work Wealthfolio stores each asset's identifier as a **canonical ticker** (e.g. `RY`, `AAPL`, `BTC`) plus an optional **exchange MIC code** (`XTSE`, `XNAS`, `XLON`…). That pair is provider-agnostic; the same asset can be priced by Yahoo, Alpha Vantage, Börse Frankfurt, OpenFIGI, or a custom scraper, and Wealthfolio's resolver translates the canonical pair into whatever format each provider expects. When you add an asset you can type either: - The bare ticker (`AAPL`): for US equities the MIC defaults to `XNAS`. - A Yahoo-style suffix (`RY.TO`): Wealthfolio parses the suffix into a ticker + MIC. - A MIC-qualified form (`RY @ XTSE`): same outcome. If a specific provider expects a different symbol for an asset (one of your custom scrapers fetches `SHOP` as `SHOP-CA`, say), set a **per-provider override** on the asset's Market Data tab. The canonical symbol stays clean; the override is used only when querying that one provider. ### Built-in providers | Provider | Covers | Symbol format | | --- | --- | --- | | **Yahoo Finance** | Equities, ETFs, crypto, FX, commodities | Ticker + Yahoo suffix (`RY.TO`, `BTC-USD`, `EURUSD=X`) | | **Alpha Vantage** | Equities, crypto, FX (BYO key) | Ticker + Alpha Vantage exchange code | | **Finnhub** | Equities, company profiles (BYO key) | Bare ticker | | **MarketData.app** | Equities (BYO key) | Bare ticker | | **OpenFIGI** | Bonds, identifier lookup | ISIN (e.g. `US912797KL68`) | | **Börse Frankfurt** | German exchange listings | `MIC:ticker` (e.g. `XETR:SAP`) | | **Metal Price API** | Precious metals spot | Metal code | | **US Treasury Calc** | US Treasuries | CUSIP | | **Custom providers** | Anything you can hit with HTTP | User-defined template ([guide](/docs/guide/custom-providers)) | You pick the **preferred provider** per asset. If the preferred one is unhealthy or rate-limited, the resolver falls through to other capable providers. ### Yahoo Finance suffix table Yahoo is the default provider for most users, so its suffix convention is the one you'll see most often. The mappings: | Exchange | Suffix | Example | | ------------------------ | ------ | ------------- | | NASDAQ / NYSE (US) | _none_ | `AAPL` | | Toronto Stock Exchange | `.TO` | `RY.TO` | | TSX Venture (Canada) | `.V` | `WELL.V` | | London Stock Exchange | `.L` | `HSBA.L` | | Euronext Amsterdam | `.AS` | `IWDA.AS` | | Euronext Paris | `.PA` | `MC.PA` | | Euronext Brussels | `.BR` | `KBC.BR` | | Xetra (Germany) | `.DE` | `SAP.DE` | | SIX Swiss Exchange | `.SW` | `NESN.SW` | | Borsa Italiana | `.MI` | `ENI.MI` | | BME (Madrid) | `.MC` | `SAN.MC` | | Hong Kong Stock Exchange | `.HK` | `0700.HK` | | Tokyo Stock Exchange | `.T` | `7203.T` | | Shanghai | `.SS` | `601398.SS` | | Shenzhen | `.SZ` | `000333.SZ` | | Australia (ASX) | `.AX` | `BHP.AX` | | New Zealand (NZX) | `.NZ` | `FPH.NZ` | | Bovespa (São Paulo) | `.SA` | `PETR4.SA` | | BMV (Mexico) | `.MX` | `WALMEX.MX` | | Stockholm | `.ST` | `VOLV-B.ST` | | Helsinki | `.HE` | `NOKIA.HE` | | Oslo | `.OL` | `EQNR.OL` | | Copenhagen | `.CO` | `NOVO-B.CO` | | Warsaw | `.WA` | `PKO.WA` | | Vienna | `.VI` | `EBS.VI` | | Johannesburg | `.JO` | `MTN.JO` | | Tel Aviv (TASE) | `.TA` | `TEVA.TA` | | BSE (India) | `.BO` | `RELIANCE.BO` | | NSE (India) | `.NS` | `RELIANCE.NS` | For comprehensive market coverage and potential data delays, consult [Yahoo Finance's market coverage docs](https://help.yahoo.com/kb/finance-for-web/sln2310.html). #### Custom Assets You can also record custom assets without an automatic ticker lookup. This is useful for tracking assets where market data is not automatically available or for assets you wish to price manually. For such custom assets, you will need to regularly update their price information through the asset's page, typically in a "Quote" or "Pricing" section. ## Symbol lookup 1. Exact ticker (`AAPL`, `BTC-USD`). 2. Ticker + suffix (`RY.TO`). 3. First Yahoo Finance hit. ## FX rates - Pulled with the other market data from the default market data provider. - View/edit and add manual rates via `Settings→General→Exchange Rates`. - Manual rates you define take precedence over automatically fetched market data for the specified currency pairs and will be used by the system until you modify or remove them. ### Levels of Currency The application handles currencies at four distinct levels to provide accurate and flexible financial tracking: 1. **Base Currency**: This is the primary currency for your entire portfolio. All aggregated reports and overall wealth summaries are presented in this currency. You set this once, typically when you first set up the application. 2. **Account Currency**: Each account (e.g., a specific bank account, brokerage account) can have its own designated currency. This is the currency in which the account itself is denominated. For example, you might have a USD-denominated brokerage account and a EUR-denominated bank account. 3. **Asset/Holding Currency**: This refers to the currency in which a specific asset or holding is traded or valued. For instance, if you own shares of a company listed on the Tokyo Stock Exchange, the asset currency would likely be JPY. 4. **Activity Currency**: This is the currency used for a specific transaction or activity, such as a buy/sell order, dividend payment, or fee. For example, if you buy US stocks using your CAD bank account, the activity of purchasing might involve a CAD to USD conversion, and the activity currency for the purchase itself would be USD. The system uses the FX rates to convert between these different currency levels as needed for calculations, reporting, and displaying values consistently. ### Automatic Currency Unit Normalization Wealthfolio automatically handles minor currency units and normalizes them to their major currency equivalents. This means you don't need to manually configure exchange rates for these conversions. For example, market data from sources like Yahoo Finance often provides prices for securities traded on the London Stock Exchange (LSE) in pence (GBp or GBX, where GBX is the official currency code for Penny Sterling) rather than pounds (GBP). The application automatically recognizes these minor units and converts them to the major currency (GBP) using the correct conversion factor. #### Supported Minor Currency Units The system automatically normalizes the following minor currency units: - **GBp, GBX** (Pence) → **GBP** (British Pound) at 0.01 conversion factor - **ZAc, ZAC** (South African Cents) → **ZAR** (South African Rand) at 0.01 conversion factor - **ILA** (Agorot) → **ILS** (Israeli New Shekel) at 0.01 conversion factor - **KWF** → **KWD** (Kuwaiti Dinar) at 0.01 conversion factor When prices are quoted in these minor units, the application automatically converts them to the major currency for accurate valuation and reporting. This resolves common discrepancies without requiring manual intervention, as previously discussed in community threads (see [GitHub Issue #107](https://github.com/wealthfolio/wealthfolio/issues/107) and [GitHub Issue #134](https://github.com/wealthfolio/wealthfolio/issues/134)). The currency conversion processes exchange rates with the following logic: - **Historical Data**: The system stores and utilizes historical exchange rates. - **Daily Rate Selection**: For a given currency pair (e.g., USD/EUR) on a specific day, if multiple rate entries exist, the system selects the rate with the latest timestamp of that day. This ensures the most up-to-date daily rate is used. - **Automatic Rate Derivation**: - **Inverse Rates**: If a rate such as USD to EUR is provided, the converter automatically calculates and makes available the inverse rate (EUR to USD). - **Transitive Rates**: The system can derive rates through a common currency. For instance, if rates for USD to EUR and EUR to GBP are known, the rate for USD to GBP can be automatically calculated. - **Identity Rates**: Converting a currency to itself (e.g., CAD to CAD) is treated as a 1:1 conversion. - **Rate Lookup Options**: - **Specific Date Lookup**: You can request an exchange rate for a precise date. - **Nearest Date Lookup**: If an exchange rate is not available for the exact specified date, the system can find and use the rate from the closest available date. It considers both past and future dates relative to your request and selects the one chronologically nearest. - **Currency Code Handling**: Currency codes (e.g., "USD", "cad", "Eur") are processed while preserving their original casing for lookups and storage. ## FX and gain/loss Multi-currency portfolios surface a subtle question: when your base currency is CAD and you hold a USD stock, is your gain in **USD** (the security's currency) or **CAD** (your reporting currency)? Wealthfolio reports both, and they can diverge significantly when FX rates move. ### Worked example You're a Canadian investor. Base currency: **CAD**. ``` 2024-01-10 BUY 10 AAPL @ $180 USD FX rate on trade date: 1 USD = 1.35 CAD Cost basis stored: $1,800 USD (asset currency) $2,430 CAD (converted at the trade-date rate) 2024-12-15 AAPL spot price: $240 USD Today's FX rate: 1 USD = 1.40 CAD Current value: $2,400 USD = unrealized gain $600 USD $3,360 CAD (10 × $240 × 1.40) = unrealized gain $930 CAD ``` The USD gain is $600 ($240 - $180) × 10. The CAD gain is $930, bigger because the USD strengthened against the CAD over the holding period. **The same trade looks different depending on which currency you look through.** This is how every multi-currency tracker works, but it surprises people because the gain moves even when the share price hasn't. ### "My gain changed but I didn't do anything" The most common reasons your base-currency gain moved without any trade activity: - **FX rate moved.** Even if the underlying stock is flat, your CAD-denominated gain changes as USD/CAD moves. - **Quote source changed currency unit.** Yahoo sometimes flips between GBp (pence) and GBP (pounds) for UK stocks. Wealthfolio normalizes the common units (see below). If it didn't catch one, manually override the asset currency. - **A new historical quote backfilled.** When a missing day's price arrives, the gain recalculates. ### Manual FX rates To pin a rate (e.g. you know your broker used 1.378, not the public mid-market): **Settings → General → Exchange Rates → Add manual rate**. Your rate replaces the fetched rate for that date and forward, until you remove it. --- # Core Concepts - Performance Metrics Source: https://wealthfolio.app/docs/concepts/performance-metrics
Wealthfolio employs several performance metrics and calculation methods to provide a comprehensive view of your portfolio's performance. Understanding these concepts is key to interpreting your financial progress accurately. Every performance result Wealthfolio computes is built from a daily valuation series for the selected scope (a single account, a group of accounts, or your whole portfolio). For each day the engine knows your total value, your cost basis, and any external cash flows (deposits and withdrawals). All metrics below are derived from that series. ## Return Methods Wealthfolio reports performance under one of several **return methods**, chosen automatically based on how the scope is tracked. The method tells you how the headline return was calculated. * **Time-Weighted (TWR):** Used for accounts you track by recording transactions (buys, sells, deposits, withdrawals). This is the default method for most accounts. * **Value Return:** Used for *holdings-only* accounts, where you track current positions and market value rather than a full transaction history. The return is measured from changes in unrealized gain/loss. * **Symbol Price-Based:** Used when measuring the performance of an individual symbol or asset directly from its price quotes. * **Not Applicable:** Returned when there isn't enough data to compute a meaningful return for the scope and period. ## Key Performance Metrics Here are the primary metrics used throughout the application. ### 1. Time-Weighted Return (TWR) * **Definition:** Time-Weighted Return measures the compound growth rate of a portfolio while removing the distorting effects of cash inflows (deposits) and outflows (withdrawals). It reflects the performance of the underlying investments, independent of when you added or removed money. * **Use Case:** TWR is ideal for judging an investment strategy in isolation and for comparing performance across portfolios, because it is not affected by the timing or size of your cash flows. * **Calculation Insight:** Wealthfolio computes a daily return for each day in the period and geometrically links (compounds) them. Each day's return is `(End Value + Outflows − Start Value − Inflows) / (Start Value + Inflows)`. Days where the opening base is below one currency unit are excluded from the chain to avoid distortion, and flagged in the result's data quality. ### 2. Money-Weighted Return (IRR / XIRR) * **Definition:** Money-Weighted Return, expressed as an annualized Internal Rate of Return, accounts for the size and timing of every cash flow. It is the single rate that discounts all dated cash flows so their net present value equals zero. * **Use Case:** MWR reflects your *actual* personal rate of return, because it is sensitive to when money entered or left the portfolio. Two investors holding the same fund can have very different MWRs depending on their contribution timing. * **Calculation Insight:** Wealthfolio builds a dated cash-flow series — the opening value as an outflow, each deposit/withdrawal on its real date, and the closing value as an inflow — and solves for the rate using a bisection (XIRR) solver. Time is measured in years of `365.25` days. The result is reported both as an annualized rate and as a period rate scaled back to the selected window. MWR is unavailable when the cash flows never change sign. ### 3. Value Return (Simple Return) * **Definition:** A straightforward measure of gain or loss relative to the starting value, after backing out net cash flows: `(End Value − Start Value − Net Cash Flows) / Start Value`. For holdings-only accounts it is derived from the change in unrealized gain/loss relative to the starting market value (or to cost basis for an all-time view). * **Use Case:** Provides a quick, intuitive sense of overall return for the period. * **Limitations:** Because it does not account for *when* cash flows occurred, it can be misleading when there are large deposits or withdrawals mid-period. Prefer TWR or MWR in those cases. ### 4. Annualized Return * **Definition:** Annualized Return restates a period return as an equivalent compounded yearly rate, standardizing returns across different time spans for comparison. * **Use Case:** Lets you compare investments held for different lengths of time on a common yearly basis. Wealthfolio reports annualized versions of TWR, MWR, and Value Return. * **Calculation Insight:** Calculated as `(1 + Period Return)^(365.25 / Days) − 1`. Returns of −100% or worse are capped at −100%. For very short periods, annualizing magnifies noise and should be read with caution. ### 5. Volatility * **Definition:** Volatility measures how much returns fluctuate — the standard deviation of returns. Higher volatility means larger swings in value in either direction. * **Use Case:** A common proxy for risk. Higher volatility generally indicates a riskier, less predictable investment. * **Calculation Insight:** Wealthfolio takes the daily **log returns**, computes their sample standard deviation, then annualizes by multiplying by the square root of `365.25` (calendar days per year). At least two valid daily returns are required. ### 6. Maximum Drawdown * **Definition:** Maximum Drawdown is the largest peak-to-trough percentage decline in value over the period. * **Use Case:** Captures the worst loss an investor would have lived through, a tangible measure of downside risk. * **Calculation Insight:** The engine tracks the running peak of cumulative return and records the largest drop from any peak to a subsequent low. Alongside the drawdown percentage it reports the **peak date**, **trough date**, **recovery date** (when value returned to the prior peak, if it did), and the **drawdown duration** in days. ### 7. Performance Attribution * **Definition:** Attribution breaks the period's total change in value into its economic drivers, so you can see *why* your value moved, not just by how much. * **Components:** Contributions, distributions, income (e.g. dividends), realized P&L, unrealized P&L change, FX effect (currency movement for non-base-currency holdings), fees, taxes, and a **residual** that captures anything not explained by the other components. * **Calculation Insight:** Each component is computed on a best-effort basis from your activities, lot disposals, and daily valuations. The residual is the difference between the actual change in value and the sum of the explained components; if it grows beyond a small tolerance, the result is flagged as partially unreliable in the data quality. ### 8. Gain/Loss Amount * **Definition:** The absolute monetary gain or loss for the scope over the period. * **Calculation Insight:** `Total Value − Net Contribution`. This is the headline figure shown on account and portfolio cards. ### 9. Cumulative Return & Portfolio Weight * **Cumulative Return:** A simple percentage of gain over money put in, calculated as `Total Gain/Loss / Net Contribution`. Used on account summary cards for a fast read of overall return. * **Portfolio Weight:** The share of total portfolio value an account represents — `Account Value (base currency) / Total Portfolio Value (base currency)` — useful for understanding allocation and concentration. ## Data Quality Because returns depend on the completeness of your data, every performance result carries a **data quality** status: * **OK:** The result was computed cleanly with no caveats. * **Partial:** The result is usable but carries warnings — for example, cash flows had to be inferred from net-contribution deltas, FX provenance was incomplete, or the attribution residual exceeded tolerance. * **No Data:** There wasn't enough valuation history to compute the metric. * **Not Applicable:** The metric doesn't apply to this scope or period (for example, TWR when no period opens with a positive value of at least one currency unit). When a result is Partial, Wealthfolio surfaces the specific warnings and "not applicable" reasons so you can judge how much to trust the numbers. ## Multi-Currency Handling When a scope spans more than one currency, Wealthfolio can compute returns on a base-currency basis. Currency movements are isolated into the **FX effect** attribution component, so you can separate genuine investment performance from gains or losses caused purely by exchange-rate changes. See [Market Data & FX](/docs/concepts/market-data-and-fx) for how rates are sourced. ## Understanding the Differences * **TWR vs. MWR:** TWR judges investment strategy in isolation from cash-flow timing; MWR tells you your actual personal rate of return, influenced by when you added or withdrew funds. * **History vs. Summary:** A full performance history gives a deep dive with a return series, risk metrics, and attribution. A performance summary gives a quick, high-level gain/loss and return figure. * **Symbol vs. Account Performance:** Symbol performance is based purely on price quotes from market data (dividends are excluded unless the quote series is total-return adjusted). Account performance incorporates your actual transactions, cash flows, holdings, fees, and taxes. By understanding these metrics and how they are calculated, you can gain deeper insight into your investment journey with Wealthfolio.
--- # Tracking Modes Source: https://wealthfolio.app/docs/concepts/tracking-modes import { ListChecks, Camera, AlertTriangle } from 'lucide-react'; Wealthfolio offers two ways to track your investment accounts. The choice comes down to how much detail you want to maintain and what metrics matter to you. --- ## Why Two Modes? Maintaining a complete transaction history takes effort. Every buy, sell, dividend, deposit, and withdrawal must be recorded accurately with correct dates and amounts. Some users want this level of detail for precise performance analytics and tax reporting. Others just want to track their net worth without the bookkeeping overhead. Wealthfolio lets you choose the approach that fits your needs. You can use different modes for different accounts within the same portfolio. --- ## The Two Modes

Transactions Mode

Performance Tracking

Track every trade for full performance analytics. Wealthfolio calculates your current holdings from this activity history.

Best for:

What you get:

Note: Requires tracking all transactions. Gaps in history will lead to incorrect balances and returns.

Holdings Mode

Value Tracking

Enter or import your current positions directly—how many shares of each security you hold. No trade history needed.

Best for:

What you get:

Note: Requires maintaining holdings/positions as they change. No cashflow-adjusted performance.

--- ## Comparison | | Transactions | Holdings | | ------------------------ | ------------------------ | --------------------- | | Holdings come from | Your trade history | Direct entry | | Performance tracking | Full (cashflow-adjusted) | Price changes only | | Total return calculation | Yes | No | | Setup effort | More (enter all trades) | Less (enter balances) | | Maintenance | Record new trades | Update positions | --- ## Choosing the Right Mode ### Choose Transactions if: - You have complete transaction history (or can get it via CSV export or broker sync) - You want to track true investment performance including dividends, contributions, and withdrawals - You need tax reporting features like cost basis and capital gains - You're setting up broker sync (transactions are imported automatically) ### Choose Holdings if: - You only know your current positions, not how you got there - It's an old account and reconstructing history isn't practical - It's a retirement account (401k, pension) where transaction details aren't accessible - You just want to track net worth without detailed performance metrics - You want the fastest possible setup --- ## Switching Between Modes You can change an account's tracking mode at any time in account settings. However, switching from Holdings to Transactions has important implications:
Switching from Transactions to Holdings is simpler—your transaction history is preserved but holdings will be managed through snapshots going forward. --- ## How It Affects Your Workflow ### With Transactions Mode 1. **Adding data:** Record individual trades (BUY, SELL), income (DIVIDEND, INTEREST), and cash movements (DEPOSIT, WITHDRAWAL) 2. **Importing:** Use CSV import or broker sync to bring in transaction history 3. **Holdings:** Calculated automatically from your transaction history 4. **Updates:** Record new trades as they happen ### With Holdings Mode 1. **Adding data:** Enter your current positions directly (symbol, quantity, optionally price paid) 2. **Importing:** Import holdings snapshots showing positions at a point in time 3. **Holdings:** What you enter is what you see 4. **Updates:** Periodically update positions when they change, or sync automatically --- ## Frequently Asked Questions **Can I use both modes in the same portfolio?** Yes. Each account has its own tracking mode. Use Transactions for your main brokerage and Holdings for your 401(k)—they'll all roll up into your portfolio totals. **What if I have partial transaction history?** Use Transfer Holdings (TRANSFER_IN activity type) to bring in positions at a point in time, then record new transactions going forward. This lets you start with accurate holdings without needing to reconstruct every historical trade. See [Activity Types](/docs/concepts/activity-types) for details. **Will I lose data if I switch modes?** No data is deleted. When switching modes, Wealthfolio recalculates your holdings and performance based on the new mode's rules. Your transaction records and holdings snapshots are preserved. **Which mode is better for broker sync?** It depends on your broker's data accuracy. Holdings mode is recommended for simple, accurate tracking. Transactions mode can require reviewing imported transactions for ambiguous activity types that may need manual correction. --- # Wealthfolio FAQ Source: https://wealthfolio.app/docs/faq import FaqSection from '@/components/docs/faq-section'; This FAQ is the front door. For deeper explanations, follow the links inside each answer to the [concept docs](/docs/concepts) and [user guide](/docs/guide/dashboards). ## Getting started ## Data, privacy, and backup ## CSV import ## Activities, transfers, dividends, splits ## Symbols & market data ## Multi-currency & FX ## Performance metrics ## Accounts & contribution limits ## Self-hosting & Connect ## Addons & AI ## Troubleshooting --- # Glossary Source: https://wealthfolio.app/docs/glossary A short reference for the terms used across Wealthfolio's app and docs. Use it when a metric or concept is unfamiliar — every entry links out to the deeper concept doc where applicable. --- ## Performance & returns ### TWR — Time-Weighted Return The return of your underlying investments with the impact of contributions and withdrawals stripped out. Comparable to a benchmark (e.g. SPY). Wealthfolio shows TWR at the **total portfolio** level by default. See [Performance Metrics](/docs/concepts/performance-metrics). ### MWR — Money-Weighted Return The return of *you as an investor* — includes the impact of when you bought and sold (timing decisions). Equivalent to the IRR of your cash flows. Wealthfolio shows MWR at the **per-account** level by default. ### Modified Dietz A simplified MWR approximation that's faster to compute and stable across daily recompute. Wealthfolio uses Modified Dietz under the hood for per-account return. ### Realised gain / loss The profit or loss locked in when you sell. Computed as proceeds minus the cost basis of the lots closed by the sale. ### Unrealised gain / loss The on-paper profit or loss on positions you still hold — current market value minus cost basis. Becomes realised when you sell. ### Drawdown The peak-to-trough decline of your portfolio over a period, expressed as a percentage of the peak. Useful for sizing how rough a downturn was. ### Maximum drawdown (Max DD) The largest drawdown observed over the whole period being analysed. ### Volatility The standard deviation of returns over a period. Higher numbers = bumpier ride. Wealthfolio computes daily volatility and annualises it for reporting. ### Annualised return A return rate normalised to a one-year period, so 3-month, 5-year, and YTD returns can be compared apples-to-apples. ### Benchmark A reference portfolio (often a broad index like SPY or VTI) you compare your own return to. Lets you ask "did I beat the market?" --- ## Cost basis & lots ### Cost basis What you paid for a position, used to compute realised gain/loss when you sell. In Wealthfolio, cost basis is tracked at the **lot** level. ### Lot A single unit of cost basis — usually one BUY, explicit Sell Short, DRIP, staking reward, or transferred-in position. Selling or covering closes lots in FIFO order. See [Cost Basis & Lots](/docs/concepts/cost-basis-and-lots). ### FIFO — First In, First Out The lot-selection method Wealthfolio uses: when you sell, the oldest lot is closed first. The default in most jurisdictions; matches what your broker typically reports for tax purposes. ### LIFO — Last In, First Out An alternative cost-basis method (sell the newest lots first). Used in some US tax strategies. Not yet supported in Wealthfolio; planned for a future version. ### ACB — Adjusted Cost Base Canadian tax accounting's term for the weighted-average cost per share. Not natively computed today (FIFO is used); planned for a future version. ### Weighted-Average Cost (WAC) The cost basis method where every share carries the same per-unit cost, recomputed as a running total cost ÷ total quantity after each buy — also called moving-average cost (_gleitender Durchschnittspreis_ in Austria/Germany). Common in mutual fund and Canadian tax accounting, and mandatory in some jurisdictions. Not yet computed in Wealthfolio, which uses FIFO (planned for a future version); the "average cost" shown on a holding is a display figure over your open FIFO lots, not a moving average. ### Specific identification A cost-basis method where you pick which lots to close on each sale (typically for tax-loss harvesting). Not yet supported. ### Fractional share A position quantity that isn't a whole number. Wealthfolio tracks lots to eight decimal places, so DRIP fractions, crypto satoshis, and broker fractional-share programs all work. --- ## Activities & subtypes ### Activity Wealthfolio's atomic event: every BUY, SELL, dividend, transfer, fee, etc. Activities are the source of truth — all portfolio state is derived from them. See [Activity Types](/docs/concepts/activity-types). ### Subtype A semantic variation on an activity type (e.g. DRIP on DIVIDEND, Staking Reward on INTEREST, Option Expiry on ADJUSTMENT, Position Open/Close on BUY or SELL). Wealthfolio expands subtypes into canonical postings under the hood. ### DRIP — Dividend Reinvestment Plan A dividend automatically reinvested into more shares of the same security. In Wealthfolio: DIVIDEND activity with the DRIP subtype. ### Dividend in Kind A dividend paid as additional units of the same asset. DIVIDEND activity with the Dividend in Kind subtype. For a spin-off into a different ticker, use an External `TRANSFER_IN` for the received security. ### Staking reward Crypto income received as additional tokens for staking. INTEREST activity with the Staking Reward subtype. ### Position intent An Open or Close subtype on a BUY or SELL. Used for options (BTO, STO, BTC, STC) and explicit stock/ETF short workflows. ### Sell Short A SELL activity with Open Position intent. Opens or increases a signed short lot for a stock or ETF. Plain SELL activities do not open shorts. ### Buy to Cover A BUY activity with Close Position intent. Reduces or closes an existing short stock/ETF lot. ### External flow A `TRANSFER_IN` or `TRANSFER_OUT` with the External flag checked — represents an asset or cash moving in from / out to a world Wealthfolio doesn't track (opening balances, gifts, write-offs, crypto sent to a wallet). ### Internal flow A `TRANSFER_IN` or `TRANSFER_OUT` paired with a matching activity on another Wealthfolio account. Cost basis travels between accounts; no new capital enters your portfolio. ### Net contribution The sum of capital actually deposited into your portfolio (deposits + external transfers in − withdrawals − external transfers out). Used to distinguish "did you make money?" from "did you put money in?" --- ## Symbols & market data ### Ticker A short identifier for a security (e.g. `AAPL`, `RY`, `BTC`). Wealthfolio stores a canonical ticker per asset. ### MIC — Market Identifier Code An ISO 10383 four-letter exchange code (e.g. `XNAS` for NASDAQ, `XTSE` for Toronto, `XLON` for London). Wealthfolio uses MIC + ticker as the provider-agnostic identifier for an asset. ### ISIN — International Securities Identification Number A 12-character globally-unique identifier (e.g. `US0378331005` for Apple). Required by some providers (OpenFIGI) and a fallback when ticker disambiguation is hard. ### CUSIP A 9-character US/Canada securities identifier, often used for bonds. ### FIGI — Financial Instrument Global Identifier A 12-character Bloomberg-standard global identifier. Used by the OpenFIGI provider for bond and identifier lookup. ### Provider A source of market data — Yahoo Finance, Alpha Vantage, Finnhub, OpenFIGI, Börse Frankfurt, Metal Price API, US Treasury Calc, and user-defined custom providers. See [Market Data & FX](/docs/concepts/market-data-and-fx). ### Preferred provider The provider Wealthfolio prefers when fetching quotes for a given asset. Set per asset under the Market Data tab. ### Per-provider override A symbol override stored on an asset for a specific provider — used when a provider expects a different symbol format than the canonical one (e.g. `SHOP-CA` instead of `SHOP`). ### Circuit breaker A reliability pattern in the market-data layer: after a provider fails N times in a row, it's marked unhealthy and skipped until a backoff period expires. Stops runaway retries when an external API is down. --- ## Currency & FX ### Base currency The currency every aggregated total in Wealthfolio is reported in. Set in Settings → Preferences. ### Account currency The currency a specific account is denominated in (e.g. a USD brokerage, a EUR bank account). Set at account creation and cannot be changed. ### Asset / holding currency The currency a specific security trades in (e.g. a Tokyo-listed stock has JPY asset currency). ### Activity currency The currency a specific transaction was settled in (e.g. you may buy a USD stock through your CAD account; the activity currency is USD). ### FX rate The exchange rate between two currencies on a given date. Wealthfolio uses the trade-date rate to convert each activity into base currency, then today's rate for live displays. ### Minor currency unit A sub-unit of a major currency (GBp / GBX = British pence; ZAc = South African cents; ILA = Israeli agorot). Wealthfolio normalises these to the major currency automatically at the appropriate factor. --- ## Dividends & income ### Dividend yield A security's annual dividend per share divided by its current price, expressed as a percentage. Quick proxy for income return. ### Yield on cost The annual dividend per share divided by your *cost basis* per share, not the current price. A long-time holder of a growing company will see yield on cost climb even if the market yield stays flat. ### Withholding tax Tax deducted at source on dividends or interest, typically by the broker for the issuing jurisdiction. Recorded in Wealthfolio as a TAX activity. ### Ex-dividend date The cutoff date for owning a security to receive an upcoming dividend. Buy on or after the ex-date and you miss the next payment. --- ## Account types & contribution limits ### TFSA — Tax-Free Savings Account A Canadian registered account where contributions are after-tax but growth and withdrawals are tax-free. Has an annual contribution limit. ### RRSP — Registered Retirement Savings Plan A Canadian registered account where contributions are tax-deductible and growth is tax-deferred until withdrawal. Annual contribution limit based on earned income. ### FHSA — First Home Savings Account A Canadian registered account combining TFSA-like growth with RRSP-like deductibility, earmarked for a first home purchase. ### 401(k) / Roth 401(k) US employer-sponsored retirement accounts. Traditional 401(k) is pre-tax; Roth is after-tax with tax-free withdrawals. ### IRA / Roth IRA US individual retirement accounts. Traditional IRA is pre-tax; Roth is after-tax. ### Contribution limit The annual maximum you can deposit into a registered/retirement account. See [Contribution Limits](/docs/guide/contribution-limits). ### Carry-forward room Unused contribution room from prior years that you can use in the current year (e.g. TFSA, RRSP). --- ## Portfolio planning ### FIRE — Financial Independence, Retire Early A movement / planning style focused on saving aggressively to reach financial independence well before traditional retirement age. ### Safe withdrawal rate The percentage of your portfolio you can withdraw annually with high confidence of not running out of money. The classic figure is 4% but Wealthfolio's retirement simulator lets you stress-test your own number. ### Glide path How your asset allocation shifts as you age — typically more equity early, more bonds later. ### Monte Carlo simulation A planning technique that runs your portfolio through thousands of randomised return sequences to estimate the *probability* of meeting a goal, not just a single point estimate. --- ## App concepts ### Account group A logical grouping of multiple accounts (e.g. "Joint", "Spouse RRSP") used to roll up performance and net worth on the dashboard. ### Holdings mode An account tracking style where positions are seeded with External `TRANSFER_IN` activities rather than recorded BUY-by-BUY. Faster to set up, less precise performance math. See [Tracking Modes](/docs/concepts/tracking-modes). ### Transactions mode The full account tracking style — every BUY, SELL, DIVIDEND, INTEREST recorded. Precise performance math. ### Snapshot A pre-computed portfolio state at a specific date. Wealthfolio caches snapshots at strategic dates so the dashboard doesn't replay the whole activity history on every load. Snapshots after a backdated edit are invalidated and rebuilt incrementally. ### Custom provider A user-defined market-data provider that fetches prices from any JSON API, HTML page, table, or CSV source. No coding required. See [Custom Providers](/docs/guide/custom-providers). ### Connect Wealthfolio's optional paid service. Adds automatic broker sync (via SnapTrade) and end-to-end encrypted multi-device sync. See [Connect](/connect). --- **Need a term that isn't here?** Open an issue on [GitHub](https://github.com/wealthfolio/wealthfolio/issues) — the glossary grows from real questions.