From 3b5783eb97380c486ed74be080c0323fafb92907 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Thu, 18 Sep 2025 19:11:02 +0800 Subject: [PATCH 1/2] refactor: remove additional file and CRYPTO support in akshare adapter --- WATCHLIST_API_README.md | 799 ------------------ .../adapters/assets/akshare_adapter.py | 319 ++----- python/valuecell/adapters/assets/manager.py | 1 - 3 files changed, 92 insertions(+), 1027 deletions(-) delete mode 100644 WATCHLIST_API_README.md diff --git a/WATCHLIST_API_README.md b/WATCHLIST_API_README.md deleted file mode 100644 index dd1a0906e..000000000 --- a/WATCHLIST_API_README.md +++ /dev/null @@ -1,799 +0,0 @@ -# ValueCell Watchlist API Documentation - -A comprehensive API for managing financial asset watchlists with multi-market support, real-time price data, and internationalization. - -## Table of Contents - -- [Overview](#overview) -- [Ticker Naming Conventions](#ticker-naming-conventions) -- [API Endpoints](#api-endpoints) -- [Authentication](#authentication) -- [Request/Response Format](#requestresponse-format) -- [Error Handling](#error-handling) -- [Examples](#examples) - -## Overview - -The ValueCell Watchlist API provides a complete solution for managing financial asset watchlists across multiple markets including US stocks, Hong Kong stocks, A-shares (Chinese stocks), and cryptocurrencies. The API supports real-time price data, multi-language localization, and comprehensive asset search capabilities. - -### Key Features - -- ✅ **Multi-Market Support**: US stocks (NASDAQ, NYSE), Hong Kong stocks (HKEX), Chinese A-shares (SSE, SZSE), and cryptocurrencies -- ✅ **Real-time Price Data**: Current prices, market data, and price history -- ✅ **Internationalization**: Multi-language support for asset names and descriptions -- ✅ **User Watchlists**: Create, manage, and organize multiple watchlists per user -- ✅ **Asset Search**: Powerful search with filtering by market, asset type, and country -- ✅ **RESTful API**: Standard HTTP methods with JSON responses - -## Ticker Naming Conventions - -The ValueCell system uses a standardized ticker format: `[EXCHANGE]:[SYMBOL]` - -### US Stocks -``` -NASDAQ:AAPL # Apple Inc. -NYSE:JPM # JPMorgan Chase & Co. -NYSE:JNJ # Johnson & Johnson -NASDAQ:MSFT # Microsoft Corporation -NASDAQ:GOOGL # Alphabet Inc. -``` - -### Hong Kong Stocks (HKEX) -``` -HKEX:00700 # Tencent Holdings Ltd (padded to 4 digits) -HKEX:09988 # Alibaba Group Holding Ltd -HKEX:03690 # Meituan -HKEX:01299 # AIA Group Ltd -HKEX:00005 # HSBC Holdings plc -``` - -### Chinese A-Shares -#### Shanghai Stock Exchange (SSE) -``` -SSE:600519 # Kweichow Moutai Co Ltd -SSE:600036 # China Merchants Bank Co Ltd -SSE:600000 # Pudong Development Bank Co Ltd -SSE:601318 # Ping An Insurance Group Co of China Ltd -``` - -#### Shenzhen Stock Exchange (SZSE) -``` -SZSE:000858 # Wuliangye Yibin Co Ltd -SZSE:000001 # Ping An Bank Co Ltd -SZSE:000002 # China Vanke Co Ltd -SZSE:300059 # East Money Information Co Ltd -``` - -#### Beijing Stock Exchange (BSE) -``` -BSE:430047 # Jinguan Co Ltd -BSE:832885 # Jilin Carbon Co Ltd -``` - -### Cryptocurrencies -``` -CRYPTO:BTC # Bitcoin -CRYPTO:ETH # Ethereum -CRYPTO:USDT # Tether -CRYPTO:BNB # Binance Coin -CRYPTO:ADA # Cardano -CRYPTO:SOL # Solana -``` - -### Exchange Code Reference -| Exchange Code | Full Name | Market | -|---------------|-----------|---------| -| `NASDAQ` | NASDAQ Stock Market | US | -| `NYSE` | New York Stock Exchange | US | -| `HKEX` | Hong Kong Stock Exchange | Hong Kong | -| `SSE` | Shanghai Stock Exchange | China | -| `SZSE` | Shenzhen Stock Exchange | China | -| `BSE` | Beijing Stock Exchange | China | -| `CRYPTO` | Cryptocurrency | Global | - -## API Endpoints - -Base URL: `http://localhost:8000/api/v1/watchlist` - -### 1. Search Assets - -Search for financial assets with filtering options. - -**Endpoint:** `GET /search` - -**Parameters:** -- `q` (required): Search query string -- `asset_types` (optional): Comma-separated asset types -- `exchanges` (optional): Comma-separated exchange codes -- `countries` (optional): Comma-separated country codes -- `limit` (optional): Maximum results (1-200, default: 50) -- `language` (optional): Language code for localized results - -**Example Request:** -```bash -curl -X GET "http://localhost:8000/api/v1/watchlist/search?q=Apple&limit=10&language=en-US" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Asset search completed successfully", - "data": { - "results": [ - { - "ticker": "NASDAQ:AAPL", - "asset_type": "stock", - "asset_type_display": "Stock", - "names": { - "en-US": "Apple Inc.", - "zh-Hans": "苹果公司", - "zh-Hant": "蘋果公司" - }, - "display_name": "Apple Inc.", - "exchange": "NASDAQ", - "country": "US", - "currency": "USD", - "market_status": "open", - "market_status_display": "Market Open", - "relevance_score": 0.95 - } - ], - "count": 1, - "query": "Apple", - "filters": {}, - "language": "en-US" - } -} -``` - -### 2. Get Asset Details - -Get detailed information about a specific asset. - -**Endpoint:** `GET /asset/{ticker}` - -**Parameters:** -- `ticker` (path): Asset ticker in standardized format -- `language` (optional): Language code for localized content - -**Example Request:** -```bash -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/NASDAQ:AAPL?language=en-US" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Asset details retrieved successfully", - "data": { - "ticker": "NASDAQ:AAPL", - "asset_type": "stock", - "asset_type_display": "Stock", - "names": { - "en-US": "Apple Inc.", - "zh-Hans": "苹果公司", - "zh-Hant": "蘋果公司" - }, - "display_name": "Apple Inc.", - "descriptions": { - "en-US": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide." - }, - "market_info": { - "exchange": "NASDAQ", - "country": "US", - "currency": "USD", - "timezone": "America/New_York", - "market_hours": { - "open": "09:30", - "close": "16:00" - } - }, - "source_mappings": { - "yfinance": "AAPL", - "finnhub": "AAPL" - }, - "properties": { - "sector": "Technology", - "industry": "Consumer Electronics" - }, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-15T12:00:00Z", - "is_active": true - } -} -``` - -### 3. Get Asset Price - -Get current price information for an asset. - -**Endpoint:** `GET /asset/{ticker}/price` - -**Parameters:** -- `ticker` (path): Asset ticker in standardized format -- `language` (optional): Language code for localized formatting - -**Example Request:** -```bash -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/NASDAQ:AAPL/price?language=en-US" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Asset price retrieved successfully", - "data": { - "ticker": "NASDAQ:AAPL", - "price": 185.25, - "price_formatted": "$185.25", - "currency": "USD", - "timestamp": "2024-01-15T21:00:00Z", - "volume": 45678900, - "open_price": 184.50, - "high_price": 186.75, - "low_price": 183.80, - "close_price": 185.25, - "change": 0.75, - "change_percent": 0.41, - "change_percent_formatted": "+0.41%", - "market_cap": 2890000000000, - "market_cap_formatted": "$2.89T", - "source": "yfinance" - } -} -``` - -### 4. Get User Watchlists - -Get all watchlists for a specific user. - -**Endpoint:** `GET /{user_id}` - -**Parameters:** -- `user_id` (path): User identifier - -**Example Request:** -```bash -curl -X GET "http://localhost:8000/api/v1/watchlist/user123" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Retrieved 2 watchlists", - "data": [ - { - "id": 1, - "user_id": "user123", - "name": "My Stocks", - "description": "My favorite tech stocks", - "is_default": true, - "is_public": false, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-15T12:00:00Z", - "items_count": 3, - "items": [ - { - "id": 1, - "ticker": "NASDAQ:AAPL", - "notes": "Strong quarterly results", - "order_index": 1, - "added_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-10T08:00:00Z", - "exchange": "NASDAQ", - "symbol": "AAPL" - }, - { - "id": 2, - "ticker": "HKEX:00700", - "notes": "Gaming revenue growth", - "order_index": 2, - "added_at": "2024-01-02T00:00:00Z", - "updated_at": "2024-01-02T00:00:00Z", - "exchange": "HKEX", - "symbol": "00700" - } - ] - }, - { - "id": 2, - "user_id": "user123", - "name": "Crypto Portfolio", - "description": "Cryptocurrency investments", - "is_default": false, - "is_public": false, - "created_at": "2024-01-05T00:00:00Z", - "updated_at": "2024-01-12T15:00:00Z", - "items_count": 2, - "items": [ - { - "id": 3, - "ticker": "CRYPTO:BTC", - "notes": "Long-term hold", - "order_index": 1, - "added_at": "2024-01-05T00:00:00Z", - "updated_at": "2024-01-05T00:00:00Z", - "exchange": "CRYPTO", - "symbol": "BTC" - } - ] - } - ] -} -``` - -### 5. Get Specific Watchlist - -Get a specific watchlist by name with optional price data. - -**Endpoint:** `GET /{user_id}/{watchlist_name}` - -**Parameters:** -- `user_id` (path): User identifier -- `watchlist_name` (path): Watchlist name -- `include_prices` (optional): Include current prices (default: true) -- `language` (optional): Language code for localized content - -**Example Request:** -```bash -curl -X GET "http://localhost:8000/api/v1/watchlist/user123/My%20Stocks?include_prices=true&language=en-US" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Watchlist retrieved successfully", - "data": { - "id": 1, - "user_id": "user123", - "name": "My Stocks", - "description": "My favorite tech stocks", - "is_default": true, - "is_public": false, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-15T12:00:00Z", - "items_count": 2, - "items": [ - { - "id": 1, - "ticker": "NASDAQ:AAPL", - "notes": "Strong quarterly results", - "order_index": 1, - "added_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-10T08:00:00Z", - "exchange": "NASDAQ", - "symbol": "AAPL" - }, - { - "id": 2, - "ticker": "HKEX:00700", - "notes": "Gaming revenue growth", - "order_index": 2, - "added_at": "2024-01-02T00:00:00Z", - "updated_at": "2024-01-02T00:00:00Z", - "exchange": "HKEX", - "symbol": "00700" - } - ] - } -} -``` - -### 6. Create Watchlist - -Create a new watchlist for a user. - -**Endpoint:** `POST /{user_id}` - -**Parameters:** -- `user_id` (path): User identifier - -**Request Body:** -```json -{ - "name": "Tech Stocks", - "description": "Technology sector investments", - "is_default": false, - "is_public": false -} -``` - -**Example Request:** -```bash -curl -X POST "http://localhost:8000/api/v1/watchlist/user123" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Tech Stocks", - "description": "Technology sector investments", - "is_default": false, - "is_public": false - }' -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Watchlist created successfully", - "data": { - "id": 3, - "user_id": "user123", - "name": "Tech Stocks", - "description": "Technology sector investments", - "is_default": false, - "is_public": false, - "created_at": "2024-01-15T12:30:00Z", - "updated_at": "2024-01-15T12:30:00Z", - "items_count": 0, - "items": [] - } -} -``` - -### 7. Add Stock to Watchlist - -Add a stock to a user's watchlist. - -**Endpoint:** `POST /{user_id}/stocks` - -**Parameters:** -- `user_id` (path): User identifier - -**Request Body:** -```json -{ - "ticker": "SSE:600519", - "watchlist_name": "My Stocks", - "notes": "Chinese liquor company with strong brand" -} -``` - -**Example Request:** -```bash -curl -X POST "http://localhost:8000/api/v1/watchlist/user123/stocks" \ - -H "Content-Type: application/json" \ - -d '{ - "ticker": "SSE:600519", - "watchlist_name": "My Stocks", - "notes": "Chinese liquor company with strong brand" - }' -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Stock added to watchlist successfully", - "data": { - "ticker": "SSE:600519", - "user_id": "user123", - "watchlist_name": "My Stocks", - "notes": "Chinese liquor company with strong brand" - } -} -``` - -### 8. Remove Stock from Watchlist - -Remove a stock from a user's watchlist. - -**Endpoint:** `DELETE /{user_id}/stocks/{ticker}` - -**Parameters:** -- `user_id` (path): User identifier -- `ticker` (path): Stock ticker to remove -- `watchlist_name` (optional): Watchlist name (uses default if not provided) - -**Example Request:** -```bash -curl -X DELETE "http://localhost:8000/api/v1/watchlist/user123/stocks/SSE:600519?watchlist_name=My%20Stocks" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Stock removed from watchlist successfully", - "data": { - "ticker": "SSE:600519", - "user_id": "user123", - "watchlist_name": "My Stocks" - } -} -``` - -### 9. Update Stock Notes - -Update notes for a stock in a watchlist. - -**Endpoint:** `PUT /{user_id}/stocks/{ticker}/notes` - -**Parameters:** -- `user_id` (path): User identifier -- `ticker` (path): Stock ticker -- `watchlist_name` (optional): Watchlist name (uses default if not provided) - -**Request Body:** -```json -{ - "notes": "Updated analysis: Strong growth potential in AI sector" -} -``` - -**Example Request:** -```bash -curl -X PUT "http://localhost:8000/api/v1/watchlist/user123/stocks/NASDAQ:AAPL/notes?watchlist_name=My%20Stocks" \ - -H "Content-Type: application/json" \ - -d '{ - "notes": "Updated analysis: Strong growth potential in AI sector" - }' -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Stock notes updated successfully", - "data": { - "ticker": "NASDAQ:AAPL", - "user_id": "user123", - "notes": "Updated analysis: Strong growth potential in AI sector", - "watchlist_name": "My Stocks" - } -} -``` - -### 10. Delete Watchlist - -Delete a user's watchlist. - -**Endpoint:** `DELETE /{user_id}/{watchlist_name}` - -**Parameters:** -- `user_id` (path): User identifier -- `watchlist_name` (path): Watchlist name to delete - -**Example Request:** -```bash -curl -X DELETE "http://localhost:8000/api/v1/watchlist/user123/Tech%20Stocks" -``` - -**Example Response:** -```json -{ - "code": 0, - "msg": "Watchlist deleted successfully", - "data": { - "user_id": "user123", - "watchlist_name": "Tech Stocks" - } -} -``` - -## Authentication - -Currently, the API does not require authentication. User identification is handled through the `user_id` parameter in the URL path. In production environments, you should implement proper authentication and authorization mechanisms. - -## Request/Response Format - -### Standard Response Format - -All API responses follow a consistent format: - -```json -{ - "code": 0, // Status code (0 = success, others = error) - "msg": "success", // Response message - "data": {...} // Response data (varies by endpoint) -} -``` - -### Status Codes - -| Code | Meaning | -|------|---------| -| `0` | Success | -| `400` | Bad Request | -| `401` | Unauthorized | -| `403` | Forbidden | -| `404` | Not Found | -| `500` | Internal Server Error | - -### Content Type - -All requests and responses use `application/json` content type. - -## Error Handling - -### Error Response Format - -```json -{ - "code": 404, - "msg": "Asset 'INVALID:TICKER' not found", - "data": null -} -``` - -### Common Error Scenarios - -1. **Invalid Ticker Format** - ```json - { - "code": 400, - "msg": "Invalid ticker format. Expected 'EXCHANGE:SYMBOL'", - "data": null - } - ``` - -2. **Asset Not Found** - ```json - { - "code": 404, - "msg": "Asset 'NASDAQ:INVALID' not found", - "data": null - } - ``` - -3. **Watchlist Not Found** - ```json - { - "code": 404, - "msg": "Watchlist 'NonExistent' not found for user 'user123'", - "data": null - } - ``` - -4. **Validation Error** - ```json - { - "code": 400, - "msg": "Validation error: name field is required", - "data": null - } - ``` - -## Examples - -### Complete Workflow Example - -Here's a complete example showing how to create a watchlist and manage assets: - -```bash -# 1. Search for assets -curl -X GET "http://localhost:8000/api/v1/watchlist/search?q=Apple&limit=5" - -# 2. Create a new watchlist -curl -X POST "http://localhost:8000/api/v1/watchlist/user123" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Tech Portfolio", - "description": "My technology stock investments", - "is_default": false, - "is_public": false - }' - -# 3. Add stocks to the watchlist -curl -X POST "http://localhost:8000/api/v1/watchlist/user123/stocks" \ - -H "Content-Type: application/json" \ - -d '{ - "ticker": "NASDAQ:AAPL", - "watchlist_name": "Tech Portfolio", - "notes": "Strong iPhone sales" - }' - -curl -X POST "http://localhost:8000/api/v1/watchlist/user123/stocks" \ - -H "Content-Type: application/json" \ - -d '{ - "ticker": "HKEX:00700", - "watchlist_name": "Tech Portfolio", - "notes": "Leading Chinese tech company" - }' - -# 4. Get watchlist with current prices -curl -X GET "http://localhost:8000/api/v1/watchlist/user123/Tech%20Portfolio?include_prices=true" - -# 5. Update stock notes -curl -X PUT "http://localhost:8000/api/v1/watchlist/user123/stocks/NASDAQ:AAPL/notes" \ - -H "Content-Type: application/json" \ - -d '{ - "notes": "Strong iPhone sales + AI integration potential" - }' - -# 6. Get asset price details -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/NASDAQ:AAPL/price" - -# 7. Remove a stock from watchlist -curl -X DELETE "http://localhost:8000/api/v1/watchlist/user123/stocks/HKEX:00700?watchlist_name=Tech%20Portfolio" -``` - -### Multi-Language Example - -```bash -# Search with Chinese localization -curl -X GET "http://localhost:8000/api/v1/watchlist/search?q=苹果&language=zh-Hans" - -# Get asset details in Traditional Chinese -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/NASDAQ:AAPL?language=zh-Hant" - -# Get price data with Chinese formatting -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/SSE:600519/price?language=zh-Hans" -``` - -### Cryptocurrency Example - -```bash -# Search for cryptocurrencies -curl -X GET "http://localhost:8000/api/v1/watchlist/search?q=Bitcoin&asset_types=crypto" - -# Add Bitcoin to watchlist -curl -X POST "http://localhost:8000/api/v1/watchlist/user123/stocks" \ - -H "Content-Type: application/json" \ - -d '{ - "ticker": "CRYPTO:BTC", - "watchlist_name": "Crypto Portfolio", - "notes": "Digital gold hedge against inflation" - }' - -# Get Bitcoin price -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/CRYPTO:BTC/price" -``` - -## Development and Testing - -### Running the API Server - -```bash -# Navigate to the project directory -cd /Users/guoyuliang/Project/valuecell - -# Activate virtual environment -source python/.venv/bin/activate - -# Install dependencies -uv sync - -# Start the API server -uvicorn python.valuecell.server.main:app --host 0.0.0.0 --port 8000 --reload -``` - -### API Documentation - -Once the server is running, you can access: -- **Swagger UI**: http://localhost:8000/docs -- **ReDoc**: http://localhost:8000/redoc - -### Testing with Different Markets - -```bash -# US Stock -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/NASDAQ:MSFT/price" - -# Hong Kong Stock -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/HKEX:00700/price" - -# Chinese A-Share -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/SSE:600519/price" - -# Cryptocurrency -curl -X GET "http://localhost:8000/api/v1/watchlist/asset/CRYPTO:ETH/price" -``` - -## Data Sources - -The API integrates with multiple data sources: - -- **Yahoo Finance**: Free global stock data -- **AKShare**: Free Chinese financial data -- **TuShare**: Professional Chinese stock data (API key required) -- **Finnhub**: Professional global stock data (API key required) -- **CoinMarketCap**: Cryptocurrency data (API key required) - -## Support - -For questions, issues, or feature requests, please refer to the ValueCell project documentation or contact the development team. diff --git a/python/valuecell/adapters/assets/akshare_adapter.py b/python/valuecell/adapters/assets/akshare_adapter.py index a8e0aa9b7..f827652bd 100644 --- a/python/valuecell/adapters/assets/akshare_adapter.py +++ b/python/valuecell/adapters/assets/akshare_adapter.py @@ -81,7 +81,6 @@ def _initialize(self) -> None: "fund": AssetType.ETF, # "bond": AssetType.BOND, "index": AssetType.INDEX, - "crypto": AssetType.CRYPTO, } # Field mapping - Handle AKShare API field changes @@ -103,8 +102,7 @@ def _initialize(self) -> None: "us_stocks": { "code": ["代码", "symbol", "ticker"], "name": ["名称", "name", "short_name"], - }, - "crypto": {"code": ["symbol", "代码"], "name": ["name", "名称"]}, + } } # Exchange mapping for AKShare @@ -305,9 +303,6 @@ def search_assets(self, query: AssetSearchQuery) -> List[AssetSearchResult]: if "us_stocks" in likely_markets and len(results) < query.limit: results.extend(self._search_us_stocks_direct(search_term, query)) - if "crypto" in likely_markets and len(results) < query.limit: - results.extend(self._search_crypto_direct(search_term, query)) - if "etfs" in likely_markets and len(results) < query.limit: results.extend(self._search_etfs_direct(search_term, query)) @@ -358,7 +353,6 @@ def _determine_likely_markets( # Determine markets based on query filters if query.asset_types: type_market_map = { - AssetType.CRYPTO: "crypto", AssetType.ETF: "etfs", AssetType.STOCK: ["a_shares", "hk_stocks", "us_stocks"], } @@ -386,10 +380,10 @@ def _determine_likely_markets( # If still empty, search all markets if not likely_markets: - likely_markets = {"a_shares", "us_stocks", "hk_stocks", "crypto", "etfs"} + likely_markets = {"a_shares", "us_stocks", "hk_stocks", "etfs"} # Convert to list with priority order - priority_order = ["a_shares", "us_stocks", "hk_stocks", "crypto", "etfs"] + priority_order = ["a_shares", "us_stocks", "hk_stocks", "etfs"] result = [market for market in priority_order if market in likely_markets] logger.debug(f"Determined likely markets for '{search_term}': {result}") @@ -415,11 +409,7 @@ def _analyze_search_term_pattern(self, search_term_upper: str) -> set: # US stock/crypto pattern (letters) elif search_term_upper.isalpha() and len(search_term_upper) <= 5: - crypto_symbols = {"BTC", "ETH", "USDT", "BNB", "ADA", "XRP", "SOL", "DOT"} - if search_term_upper in crypto_symbols: - markets.add("crypto") - else: - markets.add("us_stocks") + markets.add("us_stocks") # Chinese names - prioritize A-shares elif any("\u4e00" <= char <= "\u9fff" for char in search_term_upper): @@ -427,7 +417,7 @@ def _analyze_search_term_pattern(self, search_term_upper: str) -> set: # Default case else: - markets.update(["a_shares", "us_stocks", "hk_stocks", "crypto", "etfs"]) + markets.update(["a_shares", "us_stocks", "hk_stocks", "etfs"]) return markets @@ -715,89 +705,6 @@ def _generate_us_stock_candidates(self, search_term: str) -> List[str]: return candidates[:10] - def _search_crypto_direct( - self, search_term: str, query: AssetSearchQuery - ) -> List[AssetSearchResult]: - """Search cryptocurrencies using direct queries.""" - results = [] - - # If search term looks like crypto symbol, try direct lookup - if self._is_crypto_symbol(search_term): - result = self._get_crypto_by_symbol(search_term) - if result: - results.append(result) - return results - - # For other searches, try common crypto symbols - candidate_symbols = self._generate_crypto_candidates(search_term) - - for symbol in candidate_symbols[: query.limit]: - try: - result = self._get_crypto_by_symbol(symbol) - if result and self._matches_search_term(result, search_term): - results.append(result) - except Exception as e: - logger.debug(f"Failed to get crypto info for {symbol}: {e}") - continue - - return results - - def _is_crypto_symbol(self, search_term: str) -> bool: - """Check if search term looks like a crypto symbol.""" - common_crypto = {"BTC", "ETH", "USDT", "BNB", "ADA", "XRP", "SOL", "DOT"} - return search_term.upper() in common_crypto or ( - search_term.isalpha() and 2 <= len(search_term) <= 10 - ) - - def _get_crypto_by_symbol(self, symbol: str) -> Optional[AssetSearchResult]: - """Get crypto info by symbol using direct query.""" - try: - internal_ticker = f"CRYPTO:{symbol.upper()}" - - names = { - "zh-Hans": symbol.upper(), - "zh-Hant": symbol.upper(), - "en-US": symbol.upper(), - } - - return AssetSearchResult( - ticker=internal_ticker, - asset_type=AssetType.CRYPTO, - names=names, - exchange="CRYPTO", - country="GLOBAL", - currency="USD", - market_status=MarketStatus.UNKNOWN, - relevance_score=2.0, # High relevance for direct matches - ) - - except Exception as e: - logger.debug(f"Error getting crypto info for {symbol}: {e}") - return None - - def _generate_crypto_candidates(self, search_term: str) -> List[str]: - """Generate candidate crypto symbols based on search term.""" - candidates = [] - - # Common cryptocurrencies - common_cryptos = [ - "BTC", # Bitcoin - "ETH", # Ethereum - "USDT", # Tether - "BNB", # Binance Coin - "ADA", # Cardano - "XRP", # Ripple - "SOL", # Solana - "DOT", # Polkadot - ] - - if search_term.isalpha() and len(search_term) <= 10: - candidates.append(search_term.upper()) - - candidates.extend(common_cryptos) - - return candidates[:10] - def _search_etfs_direct( self, search_term: str, query: AssetSearchQuery ) -> List[AssetSearchResult]: @@ -922,8 +829,6 @@ def get_asset_info(self, ticker: str) -> Optional[Asset]: return self._get_hk_stock_info(ticker, exchange, symbol) elif exchange in ["NASDAQ", "NYSE"]: return self._get_us_stock_info(ticker, exchange, symbol) - elif exchange == "CRYPTO": - return self._get_crypto_info(ticker, exchange, symbol) else: logger.warning(f"Unsupported exchange: {exchange}") return None @@ -937,7 +842,8 @@ def _get_a_share_info( ) -> Optional[Asset]: """Get A-share stock information.""" try: - df_info = ak.stock_individual_info_em(symbol=symbol) + # Use the new Snowball API for individual stock basic info + df_info = ak.stock_individual_basic_info_xq(symbol=symbol) if df_info is None or df_info.empty: return None @@ -949,10 +855,11 @@ def _get_a_share_info( # Create localized names names = LocalizedName() - stock_name = info_dict.get("股票名称", symbol) - names.set_name("zh-Hans", stock_name) - names.set_name("zh-Hant", stock_name) - names.set_name("en-US", stock_name) + stock_name_cn = info_dict.get("org_short_name_cn", symbol) + stock_name_en = info_dict.get("org_short_name_en", symbol) + names.set_name("zh-Hans", stock_name_cn) + names.set_name("zh-Hant", stock_name_cn) + names.set_name("en-US", stock_name_en) # Create market info market_info = MarketInfo( @@ -973,28 +880,45 @@ def _get_a_share_info( # Set source mapping asset.set_source_ticker(self.source, symbol) - # Add additional properties from AKShare + # Add additional properties from Snowball API properties = { - "stock_name": info_dict.get("股票名称"), - "stock_code": info_dict.get("股票代码"), - "listing_date": info_dict.get("上市时间"), - "total_share_capital": info_dict.get("总股本"), - "circulating_share_capital": info_dict.get("流通股本"), - "industry": info_dict.get("所处行业"), - "main_business": info_dict.get("主营业务"), - "business_scope": info_dict.get("经营范围"), - "chairman": info_dict.get("董事长"), - "general_manager": info_dict.get("总经理"), - "secretary": info_dict.get("董秘"), - "registered_capital": info_dict.get("注册资本"), - "employees": info_dict.get("员工人数"), - "province": info_dict.get("所属省份"), - "city": info_dict.get("所属城市"), - "office_address": info_dict.get("办公地址"), - "company_website": info_dict.get("公司网址"), - "email": info_dict.get("电子邮箱"), - "main_business_income": info_dict.get("主营业务收入"), - "net_profit": info_dict.get("净利润"), + "org_id": info_dict.get("org_id"), + "org_name_cn": info_dict.get("org_name_cn"), + "org_short_name_cn": info_dict.get("org_short_name_cn"), + "org_name_en": info_dict.get("org_name_en"), + "org_short_name_en": info_dict.get("org_short_name_en"), + "main_operation_business": info_dict.get("main_operation_business"), + "operating_scope": info_dict.get("operating_scope"), + "org_cn_introduction": info_dict.get("org_cn_introduction"), + "legal_representative": info_dict.get("legal_representative"), + "general_manager": info_dict.get("general_manager"), + "secretary": info_dict.get("secretary"), + "established_date": info_dict.get("established_date"), + "reg_asset": info_dict.get("reg_asset"), + "staff_num": info_dict.get("staff_num"), + "telephone": info_dict.get("telephone"), + "postcode": info_dict.get("postcode"), + "fax": info_dict.get("fax"), + "email": info_dict.get("email"), + "org_website": info_dict.get("org_website"), + "reg_address_cn": info_dict.get("reg_address_cn"), + "reg_address_en": info_dict.get("reg_address_en"), + "office_address_cn": info_dict.get("office_address_cn"), + "office_address_en": info_dict.get("office_address_en"), + "currency": info_dict.get("currency"), + "listed_date": info_dict.get("listed_date"), + "provincial_name": info_dict.get("provincial_name"), + "actual_controller": info_dict.get("actual_controller"), + "classi_name": info_dict.get("classi_name"), + "pre_name_cn": info_dict.get("pre_name_cn"), + "chairman": info_dict.get("chairman"), + "executives_nums": info_dict.get("executives_nums"), + "actual_issue_vol": info_dict.get("actual_issue_vol"), + "issue_price": info_dict.get("issue_price"), + "actual_rc_net_amt": info_dict.get("actual_rc_net_amt"), + "pe_after_issuing": info_dict.get("pe_after_issuing"), + "online_success_rate_of_issue": info_dict.get("online_success_rate_of_issue"), + "affiliate_industry": info_dict.get("affiliate_industry"), } # Filter out None values @@ -1071,37 +995,6 @@ def _get_us_stock_info( logger.error(f"Error creating US stock info for {symbol}: {e}") return None - def _get_crypto_info( - self, ticker: str, exchange: str, symbol: str - ) -> Optional[Asset]: - """Get cryptocurrency information.""" - try: - names = LocalizedName() - names.set_name("zh-Hans", symbol) - names.set_name("zh-Hant", symbol) - names.set_name("en-US", symbol) - - market_info = MarketInfo( - exchange=exchange, - country="GLOBAL", - currency="USD", - timezone="UTC", - ) - - asset = Asset( - ticker=ticker, - asset_type=AssetType.CRYPTO, - names=names, - market_info=market_info, - ) - - asset.set_source_ticker(self.source, symbol) - return asset - - except Exception as e: - logger.error(f"Error creating crypto info for {symbol}: {e}") - return None - def get_real_time_price(self, ticker: str) -> Optional[AssetPrice]: """Get real-time price data from AKShare.""" try: @@ -1114,8 +1007,6 @@ def get_real_time_price(self, ticker: str) -> Optional[AssetPrice]: return self._get_hk_stock_price(ticker, exchange, symbol) elif exchange in ["NASDAQ", "NYSE"]: return self._get_us_stock_price(ticker, exchange, symbol) - elif exchange == "CRYPTO": - return self._get_crypto_price(ticker, exchange, symbol) else: logger.warning(f"Unsupported exchange for real-time price: {exchange}") return None @@ -1129,25 +1020,25 @@ def _get_a_share_price( ) -> Optional[AssetPrice]: """Get A-share real-time price using direct query.""" try: - # Use direct real-time price query for individual stock - cache_key = f"a_share_price_{symbol}" + # Use direct real-time price query - stock_zh_a_spot_em takes no parameters + cache_key = "a_share_price_all" df_realtime = self._get_cached_data( - cache_key, self._safe_akshare_call, ak.stock_zh_a_spot_em, symbol=symbol + cache_key, self._safe_akshare_call, ak.stock_zh_a_spot_em ) if df_realtime is None or df_realtime.empty: # Fallback to individual stock info if spot price fails return self._get_a_share_price_from_info(ticker, exchange, symbol) - # If we get multiple results, find the matching one - if len(df_realtime) > 1: - stock_data = df_realtime[df_realtime["代码"] == symbol] - if stock_data.empty: - stock_info = df_realtime.iloc[0] # Use first result as fallback - else: - stock_info = stock_data.iloc[0] - else: - stock_info = df_realtime.iloc[0] + # Find the specific stock in the A-share data + # The dataframe contains all A-shares, we need to filter by stock code + stock_data = df_realtime[df_realtime["代码"] == symbol] + if stock_data.empty: + # If not found by exact match, try alternative matching + logger.warning(f"Stock {symbol} not found in A-share spot data, falling back to individual info") + return self._get_a_share_price_from_info(ticker, exchange, symbol) + + stock_info = stock_data.iloc[0] # Extract price information using safe field access current_price = self._safe_decimal_convert(stock_info.get("最新价", 0)) @@ -1194,27 +1085,45 @@ def _get_a_share_price_from_info( """Get A-share price from individual stock info as fallback.""" try: # Try to get basic price info from stock individual info - df_info = self._safe_akshare_call( - ak.stock_individual_info_em, symbol=symbol + cache_key = f"a_share_info_price_{symbol}" + df_info = self._get_cached_data( + cache_key, self._safe_akshare_call, ak.stock_individual_info_em, symbol=symbol ) if df_info is None or df_info.empty: + logger.warning(f"No individual stock info available for {symbol}") + return None + + # Convert DataFrame to dict for easier access + info_dict = {} + for _, row in df_info.iterrows(): + info_dict[row["item"]] = row["value"] + + # Extract current price from the individual info (if available) + current_price_value = info_dict.get("最新", info_dict.get("现价", 0)) + current_price = self._safe_decimal_convert(current_price_value) + + # Get market cap and other info + market_cap = self._safe_decimal_convert(info_dict.get("总市值")) + total_shares = self._safe_decimal_convert(info_dict.get("总股本")) + + if not current_price or current_price == 0: + logger.warning(f"No valid current price found for {symbol} in individual info") return None - # Create basic price info return AssetPrice( ticker=ticker, - price=Decimal("0"), # Placeholder + price=current_price, currency="CNY", timestamp=datetime.now(), - volume=None, - open_price=None, - high_price=None, - low_price=None, - close_price=Decimal("0"), - change=None, - change_percent=None, - market_cap=None, + volume=None, # Not available in individual info + open_price=None, # Not available in individual info + high_price=None, # Not available in individual info + low_price=None, # Not available in individual info + close_price=current_price, + change=None, # Not available in individual info + change_percent=None, # Not available in individual info + market_cap=market_cap, source=self.source, ) @@ -1338,22 +1247,6 @@ def _get_us_stock_price( logger.error(f"Error fetching US stock price for {symbol}: {e}") return None - def _get_crypto_price( - self, ticker: str, exchange: str, symbol: str - ) -> Optional[AssetPrice]: - """Get cryptocurrency real-time price without downloading full market data.""" - try: - # Skip downloading all crypto data - this is too expensive - # Return None for now, crypto prices should be handled by other adapters like yfinance - logger.warning( - f"Crypto price fetching disabled for AKShare to avoid full market data download for {symbol}" - ) - return None - - except Exception as e: - logger.error(f"Error fetching crypto price for {symbol}: {e}") - return None - def get_historical_prices( self, ticker: str, @@ -1378,10 +1271,6 @@ def get_historical_prices( return self._get_us_stock_historical( ticker, exchange, symbol, start_date, end_date, interval ) - elif exchange == "CRYPTO": - return self._get_crypto_historical( - ticker, exchange, symbol, start_date, end_date, interval - ) else: logger.warning(f"Unsupported exchange for historical data: {exchange}") return [] @@ -1601,33 +1490,12 @@ def _get_us_stock_historical( logger.error(f"Error fetching US stock historical data for {symbol}: {e}") return [] - def _get_crypto_historical( - self, - ticker: str, - exchange: str, - symbol: str, - start_date: datetime, - end_date: datetime, - interval: str, - ) -> List[AssetPrice]: - """Get cryptocurrency historical price data.""" - try: - # Note: AKShare crypto historical data may be limited - # For now, return empty list as detailed crypto historical API needs to be investigated - logger.warning(f"Crypto historical data not yet implemented for {symbol}") - return [] - - except Exception as e: - logger.error(f"Error fetching crypto historical data for {symbol}: {e}") - return [] - def get_supported_asset_types(self) -> List[AssetType]: """Get asset types supported by AKShare.""" return [ AssetType.STOCK, AssetType.ETF, AssetType.INDEX, - AssetType.CRYPTO, ] def _perform_health_check(self) -> Any: @@ -1746,8 +1614,6 @@ def _generate_ticker_variations(self, search_term: str) -> List[str]: [ f"NASDAQ:{search_term}", f"NYSE:{search_term}", - f"CRYPTO:{search_term}", - f"CRYPTO:{search_term}USD", ] ) @@ -1766,7 +1632,6 @@ def validate_ticker(self, ticker: str) -> bool: "HKEX": lambda s: s.isdigit() and 1 <= len(s) <= 5, "NASDAQ": lambda s: 1 <= len(s) <= 5, "NYSE": lambda s: 1 <= len(s) <= 5, - "CRYPTO": lambda s: 2 <= len(s) <= 10, } validator = validation_rules.get(exchange) diff --git a/python/valuecell/adapters/assets/manager.py b/python/valuecell/adapters/assets/manager.py index 9a6fa1ecd..5c7808196 100644 --- a/python/valuecell/adapters/assets/manager.py +++ b/python/valuecell/adapters/assets/manager.py @@ -60,7 +60,6 @@ def _set_default_priorities(self) -> None: AssetType.CRYPTO: [ DataSource.COINMARKETCAP, DataSource.YFINANCE, - DataSource.AKSHARE, ], AssetType.INDEX: [ DataSource.YFINANCE, From 525c912505b4260b5b684ebe29f9bca0c5c6610c Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Thu, 18 Sep 2025 19:12:17 +0800 Subject: [PATCH 2/2] lint --- .../adapters/assets/akshare_adapter.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/python/valuecell/adapters/assets/akshare_adapter.py b/python/valuecell/adapters/assets/akshare_adapter.py index f827652bd..289524b4e 100644 --- a/python/valuecell/adapters/assets/akshare_adapter.py +++ b/python/valuecell/adapters/assets/akshare_adapter.py @@ -102,7 +102,7 @@ def _initialize(self) -> None: "us_stocks": { "code": ["代码", "symbol", "ticker"], "name": ["名称", "name", "short_name"], - } + }, } # Exchange mapping for AKShare @@ -917,7 +917,9 @@ def _get_a_share_info( "issue_price": info_dict.get("issue_price"), "actual_rc_net_amt": info_dict.get("actual_rc_net_amt"), "pe_after_issuing": info_dict.get("pe_after_issuing"), - "online_success_rate_of_issue": info_dict.get("online_success_rate_of_issue"), + "online_success_rate_of_issue": info_dict.get( + "online_success_rate_of_issue" + ), "affiliate_industry": info_dict.get("affiliate_industry"), } @@ -1035,9 +1037,11 @@ def _get_a_share_price( stock_data = df_realtime[df_realtime["代码"] == symbol] if stock_data.empty: # If not found by exact match, try alternative matching - logger.warning(f"Stock {symbol} not found in A-share spot data, falling back to individual info") + logger.warning( + f"Stock {symbol} not found in A-share spot data, falling back to individual info" + ) return self._get_a_share_price_from_info(ticker, exchange, symbol) - + stock_info = stock_data.iloc[0] # Extract price information using safe field access @@ -1087,7 +1091,10 @@ def _get_a_share_price_from_info( # Try to get basic price info from stock individual info cache_key = f"a_share_info_price_{symbol}" df_info = self._get_cached_data( - cache_key, self._safe_akshare_call, ak.stock_individual_info_em, symbol=symbol + cache_key, + self._safe_akshare_call, + ak.stock_individual_info_em, + symbol=symbol, ) if df_info is None or df_info.empty: @@ -1102,13 +1109,14 @@ def _get_a_share_price_from_info( # Extract current price from the individual info (if available) current_price_value = info_dict.get("最新", info_dict.get("现价", 0)) current_price = self._safe_decimal_convert(current_price_value) - + # Get market cap and other info market_cap = self._safe_decimal_convert(info_dict.get("总市值")) - total_shares = self._safe_decimal_convert(info_dict.get("总股本")) if not current_price or current_price == 0: - logger.warning(f"No valid current price found for {symbol} in individual info") + logger.warning( + f"No valid current price found for {symbol} in individual info" + ) return None return AssetPrice(