From 0c7530c4f2b3fb3cbdedb9f3e2522e0bf68dd91b Mon Sep 17 00:00:00 2001 From: agatha-miao Date: Sat, 4 Apr 2026 14:10:37 +0800 Subject: [PATCH] feat: add initial AKShare A-share data vendor support --- README.md | 27 ++++ main.py | 8 +- pyproject.toml | 1 + tradingagents/dataflows/akshare_cn.py | 186 ++++++++++++++++++++++++++ tradingagents/dataflows/interface.py | 23 +++- tradingagents/default_config.py | 8 +- 6 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 tradingagents/dataflows/akshare_cn.py diff --git a/README.md b/README.md index 4cfeb4e5..a2923d92 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,33 @@ _, decision = ta.propagate("NVDA", "2026-01-15") print(decision) ``` +### A-share (China) with AKShare + +If you want to run TradingAgents on A-share symbols with AKShare data: + +1. Install dependencies: +```bash +pip install . +pip install akshare +``` +2. Switch data vendors to AKShare in your config: +```python +config["data_vendors"] = { + "core_stock_apis": "akshare", + "technical_indicators": "akshare", + "fundamental_data": "akshare", + "news_data": "akshare", +} +``` +3. Use A-share symbols like `600519` or `sh600519`: +```python +_, decision = ta.propagate("600519", "2026-01-15") +``` + +Notes: +- The current AKShare adapter provides production-ready price + basic technical indicator access. +- Fundamental/news/insider methods are scaffolded as placeholders so you can bind your preferred AKShare endpoints quickly. + See `tradingagents/default_config.py` for all configuration options. ## Contributing diff --git a/main.py b/main.py index c94fde32..293015a6 100644 --- a/main.py +++ b/main.py @@ -14,10 +14,10 @@ config["max_debate_rounds"] = 1 # Increase debate rounds # Configure data vendors (default uses yfinance, no extra API keys needed) config["data_vendors"] = { - "core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance - "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance - "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance - "news_data": "yfinance", # Options: alpha_vantage, yfinance + "core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance, akshare + "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance, akshare + "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance, akshare + "news_data": "yfinance", # Options: alpha_vantage, yfinance, akshare } # Initialize with custom config diff --git a/pyproject.toml b/pyproject.toml index 0decedb0..cbeaa9f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "tqdm>=4.67.1", "typing-extensions>=4.14.0", "yfinance>=0.2.63", + "akshare>=1.17.62", ] [project.scripts] diff --git a/tradingagents/dataflows/akshare_cn.py b/tradingagents/dataflows/akshare_cn.py new file mode 100644 index 00000000..ae520930 --- /dev/null +++ b/tradingagents/dataflows/akshare_cn.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Annotated + +import pandas as pd + + +def _load_akshare(): + try: + import akshare as ak + except ImportError as exc: + raise RuntimeError( + "AKShare is not installed. Run `pip install akshare` first." + ) from exc + return ak + + +def _normalize_a_share_symbol(symbol: str) -> str: + s = symbol.strip().lower() + if s.startswith(("sh", "sz", "bj")) and len(s) >= 8: + return s + if s.isdigit() and len(s) == 6: + if s.startswith(("6", "9")): + return f"sh{s}" + if s.startswith(("0", "2", "3")): + return f"sz{s}" + if s.startswith(("4", "8")): + return f"bj{s}" + return s + + +def _extract_code(symbol: str) -> str: + norm = _normalize_a_share_symbol(symbol) + if len(norm) >= 8 and norm[:2] in {"sh", "sz", "bj"}: + return norm[2:] + return norm + + +def get_stock_data_akshare( + symbol: Annotated[str, "A-share symbol, e.g. 600519 or sh600519"], + start_date: Annotated[str, "Start date in yyyy-mm-dd format"], + end_date: Annotated[str, "End date in yyyy-mm-dd format"], +): + ak = _load_akshare() + datetime.strptime(start_date, "%Y-%m-%d") + datetime.strptime(end_date, "%Y-%m-%d") + + code = _extract_code(symbol) + start = start_date.replace("-", "") + end = end_date.replace("-", "") + + try: + df = ak.stock_zh_a_hist( + symbol=code, + period="daily", + start_date=start, + end_date=end, + adjust="qfq", + ) + except Exception as exc: + return f"Error fetching A-share stock data for {symbol}: {exc}" + + if df is None or df.empty: + return f"No A-share data found for symbol '{symbol}' between {start_date} and {end_date}" + + rename_map = { + "日期": "Date", + "开盘": "Open", + "收盘": "Close", + "最高": "High", + "最低": "Low", + "成交量": "Volume", + "成交额": "Amount", + "振幅": "Amplitude", + "涨跌幅": "ChangePct", + "涨跌额": "Change", + "换手率": "Turnover", + } + out = df.rename(columns=rename_map) + header = ( + f"# A-share stock data for {symbol} ({code}) from {start_date} to {end_date}\n" + f"# Total records: {len(out)}\n" + f"# Data source: AKShare stock_zh_a_hist\n\n" + ) + return header + out.to_csv(index=False) + + +def get_indicator_akshare( + symbol: Annotated[str, "A-share symbol"], + indicator: Annotated[str, "technical indicator"], + curr_date: Annotated[str, "The current trading date, YYYY-mm-dd"], + look_back_days: Annotated[int, "how many days to look back"], +) -> str: + end_date = curr_date + start_date = (datetime.strptime(curr_date, "%Y-%m-%d") - timedelta(days=look_back_days * 3)).strftime("%Y-%m-%d") + raw = get_stock_data_akshare(symbol, start_date, end_date) + if raw.startswith("No A-share") or raw.startswith("Error"): + return raw + + csv_part = "\n".join(raw.splitlines()[4:]) + df = pd.read_csv(pd.io.common.StringIO(csv_part)) + if df.empty or "Close" not in df.columns: + return f"Unable to compute indicator {indicator} for {symbol}" + + close = pd.to_numeric(df["Close"], errors="coerce") + + indicator_lower = indicator.lower() + if indicator_lower == "rsi": + delta = close.diff() + gain = delta.clip(lower=0).rolling(14).mean() + loss = (-delta.clip(upper=0)).rolling(14).mean() + rs = gain / loss.replace(0, pd.NA) + series = 100 - (100 / (1 + rs)) + elif indicator_lower in {"close_10_ema", "ema10"}: + series = close.ewm(span=10, adjust=False).mean() + elif indicator_lower in {"close_50_sma", "sma50"}: + series = close.rolling(50).mean() + elif indicator_lower == "macd": + ema12 = close.ewm(span=12, adjust=False).mean() + ema26 = close.ewm(span=26, adjust=False).mean() + series = ema12 - ema26 + else: + return ( + f"AKShare adapter currently supports indicators: rsi, close_10_ema, close_50_sma, macd. " + f"Requested: {indicator}" + ) + + df["Date"] = pd.to_datetime(df["Date"], errors="coerce").dt.strftime("%Y-%m-%d") + df["indicator"] = series + window_df = df.dropna(subset=["Date"]).tail(look_back_days) + lines = [f"{d}: {v}" for d, v in zip(window_df["Date"], window_df["indicator"])] + return f"## {indicator} values for {symbol} up to {curr_date}:\n\n" + "\n".join(lines) + + +def get_fundamentals_akshare( + ticker: Annotated[str, "A-share symbol"], + curr_date: Annotated[str, "current date"] = None, +): + return ( + "AKShare fundamentals vary by endpoint and exchange. " + "Recommended: wire dedicated endpoints for balance sheet/cashflow/income statement per your data policy. " + f"Current symbol: {ticker}." + ) + + +def get_balance_sheet_akshare( + ticker: Annotated[str, "A-share symbol"], + freq: Annotated[str, "quarterly/annual"] = "quarterly", + curr_date: Annotated[str, "current date"] = None, +): + return f"AKShare balance-sheet adapter placeholder for {ticker} ({freq})." + + +def get_cashflow_akshare( + ticker: Annotated[str, "A-share symbol"], + freq: Annotated[str, "quarterly/annual"] = "quarterly", + curr_date: Annotated[str, "current date"] = None, +): + return f"AKShare cashflow adapter placeholder for {ticker} ({freq})." + + +def get_income_statement_akshare( + ticker: Annotated[str, "A-share symbol"], + freq: Annotated[str, "quarterly/annual"] = "quarterly", + curr_date: Annotated[str, "current date"] = None, +): + return f"AKShare income-statement adapter placeholder for {ticker} ({freq})." + + +def get_news_akshare( + ticker: Annotated[str, "A-share symbol"], + start_date: Annotated[str, "Start date"] = None, + end_date: Annotated[str, "End date"] = None, +): + return f"AKShare news adapter placeholder for {ticker} from {start_date} to {end_date}." + + +def get_global_news_akshare(start_date: str = None, end_date: str = None): + return f"AKShare global news adapter placeholder from {start_date} to {end_date}." + + +def get_insider_transactions_akshare( + ticker: Annotated[str, "A-share symbol"], +): + return f"A-share insider transactions adapter placeholder for {ticker}." diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index 0caf4b68..a4eac7e1 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -23,6 +23,17 @@ from .alpha_vantage import ( get_global_news as get_alpha_vantage_global_news, ) from .alpha_vantage_common import AlphaVantageRateLimitError +from .akshare_cn import ( + get_stock_data_akshare, + get_indicator_akshare, + get_fundamentals_akshare, + get_balance_sheet_akshare, + get_cashflow_akshare, + get_income_statement_akshare, + get_news_akshare, + get_global_news_akshare, + get_insider_transactions_akshare, +) # Configuration and routing logic from .config import get_config @@ -63,6 +74,7 @@ TOOLS_CATEGORIES = { VENDOR_LIST = [ "yfinance", "alpha_vantage", + "akshare", ] # Mapping of methods to their vendor-specific implementations @@ -71,41 +83,50 @@ VENDOR_METHODS = { "get_stock_data": { "alpha_vantage": get_alpha_vantage_stock, "yfinance": get_YFin_data_online, + "akshare": get_stock_data_akshare, }, # technical_indicators "get_indicators": { "alpha_vantage": get_alpha_vantage_indicator, "yfinance": get_stock_stats_indicators_window, + "akshare": get_indicator_akshare, }, # fundamental_data "get_fundamentals": { "alpha_vantage": get_alpha_vantage_fundamentals, "yfinance": get_yfinance_fundamentals, + "akshare": get_fundamentals_akshare, }, "get_balance_sheet": { "alpha_vantage": get_alpha_vantage_balance_sheet, "yfinance": get_yfinance_balance_sheet, + "akshare": get_balance_sheet_akshare, }, "get_cashflow": { "alpha_vantage": get_alpha_vantage_cashflow, "yfinance": get_yfinance_cashflow, + "akshare": get_cashflow_akshare, }, "get_income_statement": { "alpha_vantage": get_alpha_vantage_income_statement, "yfinance": get_yfinance_income_statement, + "akshare": get_income_statement_akshare, }, # news_data "get_news": { "alpha_vantage": get_alpha_vantage_news, "yfinance": get_news_yfinance, + "akshare": get_news_akshare, }, "get_global_news": { "yfinance": get_global_news_yfinance, "alpha_vantage": get_alpha_vantage_global_news, + "akshare": get_global_news_akshare, }, "get_insider_transactions": { "alpha_vantage": get_alpha_vantage_insider_transactions, "yfinance": get_yfinance_insider_transactions, + "akshare": get_insider_transactions_akshare, }, } @@ -159,4 +180,4 @@ def route_to_vendor(method: str, *args, **kwargs): except AlphaVantageRateLimitError: continue # Only rate limits trigger fallback - raise RuntimeError(f"No available vendor for '{method}'") \ No newline at end of file + raise RuntimeError(f"No available vendor for '{method}'") diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 26a4e4d2..38faf437 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -26,10 +26,10 @@ DEFAULT_CONFIG = { # Data vendor configuration # Category-level configuration (default for all tools in category) "data_vendors": { - "core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance - "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance - "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance - "news_data": "yfinance", # Options: alpha_vantage, yfinance + "core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance, akshare + "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance, akshare + "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance, akshare + "news_data": "yfinance", # Options: alpha_vantage, yfinance, akshare }, # Tool-level configuration (takes precedence over category-level) "tool_vendors": {