π Asset Plugin Guide
How to create a new Asset Source Provider to fetch prices from a new data source.
Base class: AssetSourceProvider (in backend/app/services/asset_source.py)
Plugin folder: backend/app/services/asset_source_providers/
Registry: AssetProviderRegistry
π Flow
The system calls provider methods in three distinct phases:
graph TD
subgraph "Phase 1 β Search (user looks for asset)"
S1["search(query)<br/><small>e.g., 'Apple'</small>"] --> S2["Returns: list of<br/>{identifier, display_name,<br/>currency, type}"]
end
subgraph "Phase 2 β Current Price (dashboard, refresh)"
C1["get_current_value(<br/>identifier, type, params)"] --> C2["Returns: FACurrentValue<br/><small>(price, currency, date)</small>"]
end
subgraph "Phase 3 β Historical Data (charts, analysis)"
H1["get_history_value(<br/>identifier, ..., start, end)"] --> H2["Returns: FAHistoricalData<br/><small>(prices[], currency)</small>"]
H2 --> H3["Core fills gaps<br/><small>weekends, holidays<br/>β backward_filled=True</small>"]
end
S2 ~~~ C1
C2 ~~~ H1
style S1 fill:#e3f2fd,stroke:#1565c0
style S2 fill:#e3f2fd,stroke:#1565c0
style C1 fill:#e8f5e9,stroke:#2e7d32
style C2 fill:#e8f5e9,stroke:#2e7d32
style H1 fill:#fff3e0,stroke:#e65100
style H2 fill:#fff3e0,stroke:#e65100
style H3 fill:#f3e5f5,stroke:#7b1fa2
Phase 1 is optional but strongly recommended β enables users to discover and link assets without knowing the exact identifier. See Asset Search below.
Phase 2 fetches the latest price for the dashboard or manual refresh.
Phase 3 fetches historical OHLCV data. The plugin returns only actual trading days β core fills gaps (weekends, holidays) with last known value.
Plugin responsibility: Fetch raw price data from external source. Return only actual data points (trading days).
Core responsibility: Gap filling (weekends/holidays β backward_filled=True), caching, database storage, currency conversion.
π ABC Methods
β Required (Abstract)
| Method | Signature | Description |
|---|---|---|
provider_code |
@property β str |
Unique identifier (e.g., "yfinance") |
provider_name |
@property β str |
Display name (e.g., "Yahoo Finance") |
test_cases |
@property β list[dict] |
Test cases for automated testing (identifier, identifier_type, provider_params) |
test_search_query |
@property β str \| None |
Search query for automated tests. None if search not supported. |
get_current_value(identifier, type, params) |
async β FACurrentValue |
Fetch latest price. Returns value, currency, as_of_date, source. |
get_history_value(identifier, type, params, start, end) |
async β FAHistoricalData |
Fetch historical OHLCV data for date range. Return raw data only β no gap filling. |
πͺ Strongly Recommended (Override)
Implementing search() is strongly recommended
Without search, users must know the exact asset identifier (ticker, ISIN, URL) upfront. With search, they can type a name like "Apple" and pick from results.
| Method | Default | Description |
|---|---|---|
search(query) |
Raises NOT_SUPPORTED |
Search for assets by name, ticker, or ISIN. Returns [{identifier, identifier_type, display_name, currency, type}]. |
test_search_query |
β | Query string for automated search tests (e.g., "Apple"). Return None if search not supported. |
π§ Optional (Override)
| Method | Default | Description |
|---|---|---|
get_icon |
None |
Provider icon URL for the UI |
supports_history |
True |
Set False for providers that only support current prices (e.g., web scrapers) |
validate_params(params) |
β | Validate provider-specific configuration (raise on invalid) |
generate_static_url(path) |
β | Helper to build /api/v1/uploads/plugin/asset/{path} |
π Asset Search
The search(query) method allows users to discover assets by name, ticker, or ISIN across all providers simultaneously.
βοΈ How It Works
- User types a query in the UI (e.g., "Apple", "MSCI World", "IE00B4L5Y983")
- Frontend calls
GET /api/v1/assets/provider/search?q=Apple - Backend queries all providers in parallel via
AssetSearchService.search()(asyncio.gather) - Providers that don't implement search (default raises
NOT_SUPPORTED) are silently skipped - Results are aggregated and returned to the user
π Provider Support
| Provider | search() |
test_search_query |
Notes |
|---|---|---|---|
| Yahoo Finance | β | "Apple" |
Full ticker search with caching (10 min TTL) |
| JustETF | β | "iShares Core S&P 500" |
ISIN-based search across cached ETF list |
| CSS Scraper | β | None |
No search β URL must be provided manually |
| Scheduled Investment | β | None |
Synthetic provider, no external search |
π API Endpoint
Query parameters:
q(required): Search query, min 1 characterproviders(optional): Comma-separated provider codes. Default: all providers with search support.
Response:
{
"query": "Apple",
"total_results": 5,
"results": [
{
"identifier": "AAPL",
"display_name": "Apple Inc.",
"provider_code": "yfinance",
"currency": "USD",
"asset_type": "stock"
}
],
"providers_queried": ["yfinance", "justetf"],
"providers_with_errors": []
}
- Searches are executed in parallel β one slow provider won't block others
- Provider-specific errors are logged but don't fail the entire request
- Errors are reported in
providers_with_errorsfor debugging
π» Implementation Example
# backend/app/services/asset_source_providers/my_provider.py
from datetime import date
from decimal import Decimal
from backend.app.services.asset_source import AssetSourceProvider, IdentifierType
from backend.app.services.provider_registry import register_provider, AssetProviderRegistry
from backend.app.schemas.assets import FACurrentValue, FAHistoricalData, FAPricePoint
@register_provider(AssetProviderRegistry)
class MyProvider(AssetSourceProvider):
@property
def provider_code(self) -> str:
return "my_provider"
@property
def provider_name(self) -> str:
return "My Data Provider"
@property
def test_cases(self) -> list[dict]:
return [
{"identifier": "AAPL", "identifier_type": IdentifierType.TICKER, "provider_params": None}
]
async def get_current_value(
self, identifier: str, identifier_type: IdentifierType, provider_params: dict
) -> FACurrentValue:
# Fetch latest price from your API
price = await self._fetch_price(identifier)
return FACurrentValue(
value=Decimal(str(price)),
currency="USD",
as_of_date=date.today(),
source=self.provider_name,
)
async def get_history_value(
self, identifier: str, identifier_type: IdentifierType,
provider_params: dict | None, start_date: date, end_date: date
) -> FAHistoricalData:
# Fetch historical data β return ONLY actual trading days
raw_data = await self._fetch_history(identifier, start_date, end_date)
prices = [
FAPricePoint(date=d, close=Decimal(str(p)), open=None, high=None, low=None, volume=None)
for d, p in raw_data
]
return FAHistoricalData(prices=prices, currency="USD", source=self.provider_name)
π Related Documentation
- Asset Architecture β Provider interface, caching, refresh logic
- System Providers β Built-in providers (Scheduled Investment, Manual)
- Providers List β All available providers
- Registry Pattern Overview β How the plugin system works