Skip to content

Conversation

acc-no-longer-active
Copy link

Yahoo Finance API Integration

This PR implements a Yahoo Finance API integration to provide price data for stocks and ETFs, addressing issue #9227. This integration enhances Rotki's ability to track and display prices for traditional financial assets alongside crypto assets.

Implementation Overview

The Yahoo Finance integration is implemented as a new price oracle that follows the existing oracle interfaces in Rotki. It provides both current and historical price data for stocks and ETFs. The implementation:

  1. Uses the yfinance Python library for reliable access to Yahoo Finance data
  2. Implements caching to reduce API calls and avoid rate limiting
  3. Provides robust error handling and retry mechanisms for rate limiting
  4. Adds special handling for custom assets of type 'stock' and 'etf'
  5. Extends the asset schema to support Yahoo Finance ticker symbols

Major Files Updated

New Files

  • rotkehlchen/externalapis/yahoofinance.py

    • Implements the YahooFinance class that inherits from ExternalServiceWithApiKeyOptionalDB, HistoricalPriceOracleWithCoinListInterface, and PenalizablePriceOracleMixin
    • Provides methods for querying current and historical prices
    • Implements ticker symbol extraction and normalization
    • Adds direct API access method for more reliable price fetching
    • Implements caching to reduce API calls
    • Adds special handling for custom assets with stock/ETF types
    • Implements rate limiting protection with exponential backoff
  • rotkehlchen/tests/external_apis/test_yahoofinance.py

    • Unit tests for the Yahoo Finance implementation
    • Tests ticker extraction, price querying, and error handling
    • Mocks API responses to avoid actual API calls during testing
    • Tests custom asset price querying with different ticker formats
    • Tests rate limiting retry mechanisms
  • rotkehlchen/tests/external_apis/test_yahoofinance_integration.py

    • Integration tests for the Yahoo Finance API
    • Tests actual API calls with rate limiting protection
    • Verifies the integration with the Rotki system
    • Implements test rate limiting to avoid excessive API calls
    • Tests direct API requests and response handling

Modified Files

  • rotkehlchen/assets/asset.py

    • Added yahoo_finance field to AssetWithOracles class
    • Implemented to_yahoo_finance() method to extract ticker symbols
    • Added support for custom assets with stock/etf types
    • Enhanced ticker extraction from custom asset notes
  • rotkehlchen/inquirer.py

    • Integrated Yahoo Finance with the price inquirer system
    • Added special handling for custom assets of type stock/etf
    • Implemented caching for stock/etf prices
    • Added _yahoofinance instance to the Inquirer class
    • Added _stock_etf_prices dictionary to track stock/ETF prices with timestamps
    • Implemented _should_refresh_stock_price method to determine when to refresh prices
    • Added force_refresh_stock_etf_prices method to manually refresh all stock/ETF prices
    • Enhanced custom asset price handling to work with Yahoo Finance
  • rotkehlchen/balances/manual.py

    • Enhanced add_manually_tracked_balances to handle custom assets of type stock/etf
    • Added special price handling for stock/ETF custom assets
    • Implemented price extraction from manual balance entries
    • Added logging for custom asset price tracking
    • Modified price storage mechanism to work with Yahoo Finance
  • rotkehlchen/tests/unit/test_manual_balances.py

    • Added tests for manually tracked balances with custom stock/ETF assets
    • Tests price extraction and storage for custom assets
    • Tests editing manually tracked balances with custom assets
    • Tests price updates for custom assets
  • rotkehlchen/tests/api/test_custom_assets.py

    • Enhanced tests for custom assets to include stock/ETF types
    • Tests API endpoints for custom asset creation and modification
    • Tests custom asset type validation
  • rotkehlchen/oracles/structures.py

    • Added YAHOOFINANCE to CurrentPriceOracle enum
  • rotkehlchen/types.py

    • Added YAHOO_FINANCE to ExternalService enum
  • rotkehlchen/rotkehlchen.py

    • Initialized Yahoo Finance instance alongside other price oracles
  • rotkehlchen/history/price.py

    • Integrated Yahoo Finance with the price historian
  • frontend/app/src/locales/en.json

    • Added localization for "Yahoo Finance"
  • frontend/app/src/components/helper/PrioritizedListEntry.vue

    • Added Yahoo Finance to the price oracle UI components

Integration Details

Yahoo Finance API Integration

The Yahoo Finance API integration is designed to work seamlessly with Rotki's existing price oracle system. The implementation:

  1. Follows Oracle Interface: Implements the required interfaces (HistoricalPriceOracleWithCoinListInterface, PenalizablePriceOracleMixin) to ensure compatibility with the existing system.

  2. Robust Error Handling: Implements comprehensive error handling with detailed logging to help diagnose issues.

  3. Rate Limiting Protection: Uses exponential backoff for retry logic when rate limiting is encountered.

  4. Caching: Implements an in-memory cache to reduce API calls and improve performance.

Custom Asset Integration

The integration with custom assets is a key feature of this implementation:

  1. Ticker Extraction: Implements multiple methods to extract ticker symbols from custom assets:

    • From yahoo_finance attribute
    • From notes with "ticker:" prefix
    • From asset symbol
    • From asset name with common suffixes removed
  2. Price Storage: Enhances the manual balance system to store and retrieve prices for custom stock/ETF assets.

  3. Price Refresh Logic: Implements logic to determine when to refresh stock/ETF prices based on time elapsed.

Inquirer Integration

The integration with the Inquirer system is designed to be non-intrusive:

  1. Special Handling: Adds special handling for custom assets in the price inquirer to ensure Yahoo Finance is used for stock/ETF assets.

  2. Price Caching: Implements a dedicated caching system for stock/ETF prices to reduce API calls.

  3. Background Refresh: Adds background refresh capability for stock/ETF prices to ensure up-to-date data.

Manual Balances Integration

The integration with manual balances enhances the user experience for tracking stock/ETF assets:

  1. Price Extraction: Extracts prices from manual balance entries for custom stock/ETF assets.

  2. Price Storage: Stores prices in the custom asset price system for future use.

  3. Price Updates: Updates prices when manual balances are edited.

Testing

The implementation includes comprehensive tests:

  1. Unit Tests: Tests individual components of the Yahoo Finance implementation:

    • Ticker extraction from different asset types
    • Price querying for stocks and ETFs
    • Error handling and retry mechanisms
    • Custom asset price handling
  2. Integration Tests: Tests the integration with the Rotki system:

    • Actual API calls with rate limiting protection
    • Integration with the price inquirer
    • Integration with manual balances
    • Integration with custom assets
  3. Rate Limiting Protection: Tests are designed to avoid excessive API calls:

    • Uses persistent files to track when tests were last run
    • Skips tests if they were run recently
    • Adds random delays to avoid synchronized tests
    • Skips tests in CI environments
  4. Mock Responses: Uses mock responses for tests that don't need actual API calls.

Future Improvements

Potential future enhancements to the Yahoo Finance integration:

  1. Expanded Asset Support: Add support for more asset types beyond stocks and ETFs.

  2. Symbol Mapping Database: Implement a more comprehensive symbol mapping database for better ticker resolution.

  3. Additional Data Points: Add support for dividends, corporate actions, and other financial data.

  4. UI Enhancements: Improve the UI for displaying stock/ETF information.

  5. Batch Queries: Implement batch queries to reduce API calls further.

  6. Advanced Caching: Implement more sophisticated caching strategies with persistence.

  7. Historical Data: Enhance historical data retrieval for better performance analysis.

Related Issues

Closes #9227

@CLAassistant
Copy link

CLAassistant commented Mar 10, 2025

CLA assistant check
All committers have signed the CLA.

@acc-no-longer-active
Copy link
Author

Note: the way this works is by creating a custom asset, using the name as the public company name, the type as stock, and the note as ticker: "input"

For example:

Apple Inc
stock
ticker: AAPL

from there we can create the manual balance for the asset, which will call the apple asset we created, and then when we instantiate it with a qty, it will auto-fetch the price.
Screenshot 2025-03-08 233157
Screenshot 2025-03-08 233247
Screenshot 2025-03-08 233950

@acc-no-longer-active
Copy link
Author

Have a bunch of work stuff piling on but will be free around Thursday/Friday if anyone would like to review and provide some feedback so I can take a look and do some cleanup there then!

@LefterisJP
Copy link
Member

Hey @Kernlog thanks a lot for this work!

This is an over 3k PR. We will try to take a look but it may take some time and will most probably requires lots of rounds of back and forth between us to get merged.

Just mentioning to set the expectations straight. And also responding to make sure you don't think we forgot.

@LefterisJP
Copy link
Member

Also in general don't merge develop on the branch but rebase the branch on develop. Though most likely we would end up squashing in the end.

@acc-no-longer-active
Copy link
Author

Sounds good thanks for the note on rebasing instead @LefterisJP ! Totally understand aswell 3k PR is a good chunk to go through especially when it's not too important atm. Just wanted to mention the timings I have available this week incase someone wanted that back and fourth so they knew if I would be able to work on it or not 😆 . Ty for the response appreciate it

Copy link
Member

@LefterisJP LefterisJP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I gave it a quick look and I think it needs a lot of changes and should also be split in multiple PRs. The major things I noticed are:

Asset type

You seem to dynamically try to figure out the yahoo_finance thing each time from the asset by looking at the asset's notes. I think this is a hacky way to make it work with our current codebase, but the point is to do it properly.

A proper way would be to add a new asset type for stocks and ETFs. It would be very much like the other assets but would just need to be an asset or an ETF whose price can be determined automatically.

That asset would need to have as symbol the unique ticker of the ETF or stock. Is it the ISIN symbol that is checked by yahoofinance? Then at addition time the api would check it exists and has a price in yahoofinance and if yes would accept and create the asset.

This alone can be one PR.

Manual balances and price queries.

At that point you won't need any changes in the manual balances. What you did in this PR is extremely hacky and would not go in the codebase. To check the attributes dynamically and store it like this.

I understand from our earlier discussions that the reason you did it like this is that sometimes yahoo finance borks. And then may return no price. At that point you want to re-use the last valid price we have for the asset.

There is no need to have it in this hacky way. All this special logic you added in the inquirer needs to go away. It's way too hacky and unmaintainable.

What needs to happen is a more general adjustment for price queries.

That also should be its own separate PR

The query prices endpoitn should try to see if there is a cached price in the DB for the asset within the last X hours/days and return that. For each price we should probably also return its age in the prices response. So perhaps an int depending on how old the price (in seconds) is. 0 for current.

)
price_value = entry.amount

from rotkehlchen.types import Price
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never do imports in the middle of the code. Always at top of the file

@@ -308,6 +314,11 @@ class Inquirer:
special_tokens: set[str]
weth: EvmToken
usd: FiatAsset

# Persistent storage for custom asset prices to avoid them being wiped out
_custom_asset_prices: dict[str, Price] = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no need for all that. The cache is the DB itself. You just have to properly use it.

manualcurrent: Optional['ManualCurrentOracle'] = None,
msg_aggregator: Optional['MessagesAggregator'] = None,
) -> 'Inquirer':
"""Create or get the already existing singleton.

In case of multi processing initialization avoid using `clear`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never add code in production that is useful only for tests

asset=from_asset.identifier,
error=str(e),
)
except Exception:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should never do naked except catches in python code. Always know what exceptions your code raises.

for from_asset in from_assets:
# Special handling for custom assets (stock/ETF)
try:
if hasattr(from_asset, 'get_asset_type') and from_asset.get_asset_type() == AssetType.CUSTOM_ASSET:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to recognize what type it is we have resolve_asset_to_class. No need to be doing hacky hasattr everywhere

@@ -692,7 +781,60 @@ def _preprocess_assets_to_query(
and a list of assets that still need their prices queried.
"""
found_prices, replaced_assets, unpriced_assets = {}, {}, []

# Special handling for custom assets to ensure we use cached prices
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all this needs to go away. As i wrote in the review comment.

log.error(f"Background stock/ETF refresh process failed: {e}")

# Start the background thread
thread = threading.Thread(target=refresh_all_stocks_bg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no no no don't do that. Do not use threading in rotki. Anyway as i said all of these changes need to go away

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use yahoo finance api
4 participants