564 lines
24 KiB
Python
564 lines
24 KiB
Python
"""
|
||
akshare data adapter for TradingAgents.
|
||
Replaces yfinance as the data vendor for Chinese (A-share), US, and HK stocks.
|
||
|
||
Uses akshare (https://github.com/akfamily/akshare) which sources data from
|
||
East Money (东方财富), providing reliable access without Yahoo Finance rate limits.
|
||
"""
|
||
|
||
import pandas as pd
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional
|
||
from dateutil.relativedelta import relativedelta
|
||
import os
|
||
|
||
# Suppress akshare's internal tqdm progress bars
|
||
os.environ.setdefault('TQDM_DISABLE', '1')
|
||
|
||
import akshare as ak
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Symbol detection helpers
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def _detect_market(symbol: str) -> str:
|
||
"""Detect market: 'cn' (A-share), 'us' (US), or 'hk' (HK).
|
||
|
||
A-share: 6-digit numeric (000001, 600519)
|
||
US: alphabetic tickers (AAPL, TSLA)
|
||
HK: 5-digit numeric (00700, 09988)
|
||
"""
|
||
sym = symbol.strip().upper().replace(".", "").replace("-", "")
|
||
if sym.isdigit():
|
||
if len(sym) == 6:
|
||
return "cn"
|
||
elif len(sym) == 5:
|
||
return "hk"
|
||
return "us"
|
||
|
||
|
||
def _get_exchange_prefix(symbol: str) -> str:
|
||
"""Get exchange prefix for A-share financial statement APIs.
|
||
|
||
Shanghai: 60xxxx, 68xxxx → 'SH'
|
||
Shenzhen: 00xxxx, 30xxxx → 'SZ'
|
||
Beijing: 8xxxxx, 4xxxxx → 'BJ'
|
||
"""
|
||
sym = symbol.strip()
|
||
first = sym[0]
|
||
if first == '6':
|
||
return "SH"
|
||
elif first in ('0', '3'):
|
||
return "SZ"
|
||
elif first in ('8', '4'):
|
||
return "BJ"
|
||
return "SZ"
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Data fetching helpers
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
COLUMN_MAP = {
|
||
"日期": "date",
|
||
"开盘": "open",
|
||
"收盘": "close",
|
||
"最高": "high",
|
||
"最低": "low",
|
||
"成交量": "volume",
|
||
"成交额": "amount",
|
||
"振幅": "amplitude",
|
||
"涨跌幅": "pct_change",
|
||
"涨跌额": "change",
|
||
"换手率": "turnover",
|
||
}
|
||
|
||
|
||
def _get_cn_stock_data(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||
"""Fetch A-share OHLCV via akshare stock_zh_a_hist."""
|
||
df = ak.stock_zh_a_hist(
|
||
symbol=symbol.strip(),
|
||
period="daily",
|
||
start_date=start_date.replace("-", ""),
|
||
end_date=end_date.replace("-", ""),
|
||
adjust="qfq",
|
||
)
|
||
df = df.rename(columns=COLUMN_MAP)
|
||
needed = ["date", "open", "close", "high", "low", "volume"]
|
||
return df[[c for c in needed if c in df.columns]]
|
||
|
||
|
||
def _get_us_stock_data(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||
"""Fetch US stock OHLCV via akshare stock_us_daily (no market code needed).
|
||
|
||
stock_us_daily returns all historical data; we filter by date range here.
|
||
"""
|
||
ticker = symbol.strip().upper()
|
||
# akshare US tickers use dot notation (BRK.B), not dash (BRK-B)
|
||
if "-" in symbol:
|
||
ticker = ticker.replace("-", ".")
|
||
df = ak.stock_us_daily(symbol=ticker, adjust="qfq")
|
||
df["date"] = pd.to_datetime(df["date"])
|
||
mask = (df["date"] >= pd.Timestamp(start_date)) & (df["date"] <= pd.Timestamp(end_date))
|
||
df = df[mask]
|
||
needed = ["date", "open", "close", "high", "low", "volume"]
|
||
return df[[c for c in needed if c in df.columns]]
|
||
|
||
|
||
def _get_hk_stock_data(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||
"""Fetch HK stock OHLCV via akshare stock_hk_hist."""
|
||
df = ak.stock_hk_hist(
|
||
symbol=symbol.strip(),
|
||
period="daily",
|
||
start_date=start_date.replace("-", ""),
|
||
end_date=end_date.replace("-", ""),
|
||
adjust="qfq",
|
||
)
|
||
df = df.rename(columns=COLUMN_MAP)
|
||
needed = ["date", "open", "close", "high", "low", "volume"]
|
||
return df[[c for c in needed if c in df.columns]]
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Formatting helpers
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def _fmt_stock_data(df: pd.DataFrame, symbol: str) -> str:
|
||
"""Format OHLCV DataFrame as Markdown table string."""
|
||
if df.empty:
|
||
return f"No stock data found for {symbol} in the given date range."
|
||
df = df.tail(60).copy()
|
||
df["date"] = df["date"].astype(str)
|
||
return df.to_markdown(index=False)
|
||
|
||
|
||
def _fmt_financial(df: pd.DataFrame, statement: str, symbol: str, freq: str) -> str:
|
||
"""Format financial statement DataFrame.
|
||
|
||
Handles both wide format (A-share: rows=periods, cols=items) and
|
||
long format (US: rows=item×period, cols=ITEM_NAME+AMOUNT).
|
||
"""
|
||
if df.empty:
|
||
return f"No {freq} {statement} data found for {symbol}."
|
||
|
||
# Long format (US stocks) — pivot to wide
|
||
if "ITEM_NAME" in df.columns and "AMOUNT" in df.columns:
|
||
pivot = df.pivot_table(
|
||
index="REPORT_DATE",
|
||
columns="ITEM_NAME",
|
||
values="AMOUNT",
|
||
aggfunc="first",
|
||
)
|
||
pivot = pivot.sort_index(ascending=False)
|
||
return pivot.head(8).to_markdown()
|
||
|
||
# Wide format (A-share) — select date + key items
|
||
key_items = {
|
||
"balance sheet": [
|
||
"REPORT_DATE", "TOTAL_ASSETS", "TOTAL_LIABILITIES",
|
||
"TOTAL_EQUITY", "MONETARYFUNDS", "INVENTORY",
|
||
"TOTAL_CURRENT_ASSETS", "TOTAL_CURRENT_LIAB",
|
||
"TOTAL_NONCURRENT_ASSETS", "TOTAL_NONCURRENT_LIAB",
|
||
"SHORT_LOAN", "LONG_LOAN", "GOODWILL",
|
||
],
|
||
"cash flow": [
|
||
"REPORT_DATE", "NETCASH_OPERATE", "NETCASH_INVEST",
|
||
"NETCASH_FINANCE", "CASH_EQUIVALENT_BEGIN",
|
||
"CASH_EQUIVALENT_END", "OPERATE_INFLOW",
|
||
"OPERATE_OUTFLOW", "INVEST_INFLOW", "INVEST_OUTFLOW",
|
||
"FINANCE_INFLOW", "FINANCE_OUTFLOW",
|
||
],
|
||
"income statement": [
|
||
"REPORT_DATE", "TOTAL_OPERATE_INCOME", "OPERATE_PROFIT",
|
||
"TOTAL_PROFIT", "NET_PROFIT", "BASIC_EPS",
|
||
"TOTAL_OPERATE_COST", "OPERATE_COST",
|
||
"RESEARCH_EXPENSE", "SELL_EXPENSE", "MANAGE_EXPENSE",
|
||
"FINANCE_EXPENSE",
|
||
],
|
||
}
|
||
stat_key = statement.lower().replace("_", " ")
|
||
items = key_items.get(stat_key, [])
|
||
available = [c for c in items if c in df.columns]
|
||
if not available:
|
||
# Fallback: show all columns but limit columns to first 15
|
||
result = df.head(8).iloc[:, :15]
|
||
else:
|
||
result = df[available].head(8)
|
||
return result.to_markdown(index=False)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Public API — matches y_finance.py interface
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def get_stock_data(symbol: str, start_date: str, end_date: str) -> str:
|
||
"""Get stock OHLCV price data. Returns Markdown table string."""
|
||
try:
|
||
market = _detect_market(symbol)
|
||
if market == "cn":
|
||
df = _get_cn_stock_data(symbol, start_date, end_date)
|
||
elif market == "us":
|
||
df = _get_us_stock_data(symbol, start_date, end_date)
|
||
else:
|
||
df = _get_hk_stock_data(symbol, start_date, end_date)
|
||
return _fmt_stock_data(df, symbol)
|
||
except Exception as e:
|
||
return f"[akshare] Error fetching stock data for {symbol}: {e}"
|
||
|
||
|
||
def get_indicators(
|
||
symbol: str,
|
||
indicator: str,
|
||
curr_date: str,
|
||
look_back_days: int = 365,
|
||
) -> str:
|
||
"""Get technical indicator values. Returns formatted string with date: value lines.
|
||
|
||
Uses stockstats for calculation (same as yfinance adapter).
|
||
"""
|
||
from stockstats import wrap
|
||
|
||
# Indicator descriptions (same as yfinance)
|
||
best_ind_params = {
|
||
"close_5_sma": (
|
||
"5-day SMA: A short-term simple moving average. "
|
||
"Usage: Identify short-term trend direction and potential entry/exit signals. "
|
||
"Tips: Often combined with longer SMAs (e.g., 20-day) for crossover strategies."
|
||
),
|
||
"close_10_sma": (
|
||
"10-day SMA: A short to medium-term simple moving average. "
|
||
"Usage: Useful for gauging immediate trend strength and potential support/resistance levels."
|
||
),
|
||
"close_20_sma": (
|
||
"20-day SMA: A medium-term simple moving average. "
|
||
"Usage: Commonly used to identify the intermediate trend and key swing levels."
|
||
),
|
||
"close_50_sma": (
|
||
"50-day SMA: A medium to long-term simple moving average. "
|
||
"Usage: Watched by many traders to gauge the intermediate trend strength."
|
||
),
|
||
"close_200_sma": (
|
||
"200-day SMA: A long-term simple moving average. "
|
||
"Usage: Often used to determine the overall bull/bear market trend."
|
||
),
|
||
"close_5_ema": (
|
||
"5-day EMA: A short-term exponential moving average giving more weight to recent prices. "
|
||
"Usage: Provides early signals for short-term trend changes."
|
||
),
|
||
"close_10_ema": (
|
||
"10-day EMA: A short to medium-term exponential moving average. "
|
||
"Usage: Reacts faster to price changes than SMA; good for active trading strategies."
|
||
),
|
||
"close_20_ema": (
|
||
"20-day EMA: A medium-term exponential moving average. "
|
||
"Usage: Commonly watched by swing and short-term traders for trend confirmation."
|
||
),
|
||
"close_50_ema": (
|
||
"50-day EMA: A medium to long-term exponential moving average. "
|
||
"Usage: Offers a balance between responsiveness and smoothing for trend analysis."
|
||
),
|
||
"close_200_ema": (
|
||
"200-day EMA: A long-term exponential moving average. "
|
||
"Usage: Often used by longer-term traders to gauge major trend turns."
|
||
),
|
||
"rsi": (
|
||
"RSI (14): The Relative Strength Index measures momentum on a scale of 0 to 100. "
|
||
"Usage: Identify overbought (>70) or oversold (<30) conditions. "
|
||
"Tips: Combine with trend indicators; divergences between RSI and price can signal reversals."
|
||
),
|
||
"rsi_6": (
|
||
"RSI (6): A faster-setting RSI. "
|
||
"Usage: More sensitive and generates earlier signals, ideal for quick momentum shifts."
|
||
),
|
||
"rsi_14": (
|
||
"RSI (14): The standard Relative Strength Index. "
|
||
"Usage: Same as RSI; widely used for identifying overbought/oversold and reversals."
|
||
),
|
||
"macd": (
|
||
"MACD (Moving Average Convergence Divergence): Trend-following momentum indicator. "
|
||
"Usage: Shows the relationship between two EMAs. "
|
||
"Tips: Crossovers of the MACD and signal line, as well as divergence from price, can indicate trades."
|
||
),
|
||
"macds": (
|
||
"MACD Signal: The 9-day EMA of the MACD line. "
|
||
"Usage: Acts as a trigger for buy/sell signals when the MACD line crosses above or below the signal line."
|
||
),
|
||
"macdh": (
|
||
"MACD Histogram: The difference between MACD and signal line. "
|
||
"Usage: Illustrates changes in momentum; a rising histogram indicates increasing momentum."
|
||
),
|
||
"boll": (
|
||
"Bollinger Bands Middle: 20-day SMA central line. "
|
||
"Usage: The baseline for volatility bands. "
|
||
"Tips: Prices reverting to the middle may suggest fading momentum."
|
||
),
|
||
"boll_ub": (
|
||
"Bollinger Bands Upper: Upper volatility band (2 std dev above middle). "
|
||
"Usage: May indicate overbought conditions. "
|
||
"Tips: Used with lower band to identify squeeze and breakout patterns."
|
||
),
|
||
"boll_lb": (
|
||
"Bollinger Bands Lower: Lower volatility band (2 std dev below middle). "
|
||
"Usage: May indicate oversold conditions. "
|
||
"Tips: Price touching/extending beyond the band can signal strong trend continuation."
|
||
),
|
||
"atr": (
|
||
"ATR: Average True Range measures market volatility. "
|
||
"Usage: High ATR indicates increased volatility; useful for setting stop-losses and position sizing."
|
||
),
|
||
"vwma": (
|
||
"VWMA: A moving average weighted by volume. "
|
||
"Usage: Confirm trends by integrating price action with volume data. "
|
||
"Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses."
|
||
),
|
||
"mfi": (
|
||
"MFI: The Money Flow Index is a momentum indicator that uses both price and volume to measure buying and selling pressure. "
|
||
"Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. "
|
||
"Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals."
|
||
),
|
||
}
|
||
|
||
if indicator not in best_ind_params:
|
||
raise ValueError(
|
||
f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}"
|
||
)
|
||
|
||
end_date = datetime.strptime(curr_date, "%Y-%m-%d")
|
||
start_date = end_date - relativedelta(days=look_back_days + 200) # buffer for SMA calc
|
||
|
||
try:
|
||
# Fetch OHLCV via akshare
|
||
market = _detect_market(symbol)
|
||
if market == "cn":
|
||
df = _get_cn_stock_data(symbol, start_date.strftime("%Y-%m-%d"), curr_date)
|
||
elif market == "us":
|
||
df = _get_us_stock_data(symbol, start_date.strftime("%Y-%m-%d"), curr_date)
|
||
else:
|
||
df = _get_hk_stock_data(symbol, start_date.strftime("%Y-%m-%d"), curr_date)
|
||
|
||
# Prepare for stockstats (expects capitalized column names)
|
||
df = df.rename(columns={
|
||
"date": "Date", "open": "Open", "close": "Close",
|
||
"high": "High", "low": "Low", "volume": "Volume",
|
||
})
|
||
sdf = wrap(df)
|
||
sdf["Date"] = pd.to_datetime(sdf["Date"]).dt.strftime("%Y-%m-%d")
|
||
|
||
# Trigger calculation
|
||
sdf[indicator]
|
||
|
||
# Build dict: date → value
|
||
result_map = {}
|
||
for _, row in sdf.iterrows():
|
||
val = row[indicator]
|
||
result_map[row["Date"]] = f"{val:.4f}" if not pd.isna(val) and isinstance(val, float) else str(val) if not pd.isna(val) else "N/A"
|
||
|
||
except Exception as e:
|
||
# Fallback: date-by-date calculation (less efficient but more resilient)
|
||
print(f"[akshare] Bulk indicator calc failed: {e}, falling back to per-date")
|
||
ind_string = ""
|
||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||
fallback_dt = curr_date_dt
|
||
while fallback_dt >= end_date - relativedelta(days=look_back_days):
|
||
indicator_value = _get_indicator_single(symbol, indicator, fallback_dt.strftime("%Y-%m-%d"))
|
||
ind_string += f"{fallback_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
||
fallback_dt -= relativedelta(days=1)
|
||
|
||
result_str = (
|
||
f"## {indicator} values from "
|
||
f"{end_date - relativedelta(days=look_back_days):%Y-%m-%d} to {curr_date}:\n\n"
|
||
+ ind_string
|
||
+ "\n\n"
|
||
+ best_ind_params.get(indicator, "")
|
||
)
|
||
return result_str
|
||
|
||
# Build output string
|
||
ind_string = ""
|
||
current_dt = end_date
|
||
before = end_date - relativedelta(days=look_back_days)
|
||
while current_dt >= before:
|
||
date_str = current_dt.strftime("%Y-%m-%d")
|
||
value = result_map.get(date_str, "N/A: Not a trading day (weekend or holiday)")
|
||
ind_string += f"{date_str}: {value}\n"
|
||
current_dt -= relativedelta(days=1)
|
||
|
||
result_str = (
|
||
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
|
||
+ ind_string
|
||
+ "\n\n"
|
||
+ best_ind_params.get(indicator, "")
|
||
)
|
||
return result_str
|
||
|
||
|
||
def _get_indicator_single(symbol: str, indicator: str, date_str: str) -> str:
|
||
"""Fallback: calculate a single indicator value for one date."""
|
||
from stockstats import wrap
|
||
try:
|
||
end_date = datetime.strptime(date_str, "%Y-%m-%d")
|
||
start_date = end_date - relativedelta(days=400)
|
||
market = _detect_market(symbol)
|
||
if market == "cn":
|
||
df = _get_cn_stock_data(symbol, start_date.strftime("%Y-%m-%d"), date_str)
|
||
elif market == "us":
|
||
df = _get_us_stock_data(symbol, start_date.strftime("%Y-%m-%d"), date_str)
|
||
else:
|
||
df = _get_hk_stock_data(symbol, start_date.strftime("%Y-%m-%d"), date_str)
|
||
|
||
df = df.rename(columns={
|
||
"date": "Date", "open": "Open", "close": "Close",
|
||
"high": "High", "low": "Low", "volume": "Volume",
|
||
})
|
||
sdf = wrap(df)
|
||
sdf["Date"] = pd.to_datetime(sdf["Date"]).dt.strftime("%Y-%m-%d")
|
||
sdf[indicator]
|
||
matching = sdf[sdf["Date"].str.startswith(date_str)]
|
||
if not matching.empty:
|
||
val = matching[indicator].values[0]
|
||
return f"{val:.4f}" if isinstance(val, float) else str(val)
|
||
return "N/A: Not a trading day (weekend or holiday)"
|
||
except Exception as e:
|
||
return f"N/A (error: {e})"
|
||
|
||
|
||
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
||
"""Get company fundamentals. Returns Markdown string."""
|
||
try:
|
||
market = _detect_market(ticker)
|
||
|
||
if market == "cn":
|
||
df = ak.stock_individual_info_em(symbol=ticker.strip())
|
||
lines = [f"# Fundamentals for {ticker}\n"]
|
||
for _, row in df.head(30).iterrows():
|
||
lines.append(f"- **{row['item']}**: {row['value']}")
|
||
return "\n".join(lines)
|
||
|
||
elif market == "us":
|
||
t = ticker.strip().upper().replace("-", ".")
|
||
df = ak.stock_individual_basic_info_us_xq(symbol=t)
|
||
lines = [f"# Fundamentals for {ticker}\n"]
|
||
for _, row in df.head(25).iterrows():
|
||
lines.append(f"- **{row['item']}**: {row['value']}")
|
||
return "\n".join(lines)
|
||
|
||
else: # HK
|
||
try:
|
||
df = ak.stock_hk_company_profile_em(symbol=ticker.strip())
|
||
return df.to_markdown(index=False)
|
||
except Exception:
|
||
return f"Fundamentals not available for HK stock {ticker} via akshare."
|
||
|
||
except Exception as e:
|
||
return f"[akshare] Error fetching fundamentals for {ticker}: {e}"
|
||
|
||
|
||
def get_balance_sheet(ticker: str, freq: str = "annual", curr_date: str = None) -> str:
|
||
"""Get balance sheet. freq: 'annual' or 'quarterly'. Returns Markdown string."""
|
||
try:
|
||
market = _detect_market(ticker)
|
||
|
||
if market == "cn":
|
||
prefix = _get_exchange_prefix(ticker)
|
||
df = ak.stock_balance_sheet_by_report_em(symbol=f"{prefix}{ticker.strip()}")
|
||
if freq in ("annual", "yearly"):
|
||
df = df[df["REPORT_TYPE"] == "年报"]
|
||
else:
|
||
df = df[df["REPORT_TYPE"].isin(["一季报", "中报", "三季报"])]
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "balance sheet", ticker, freq)
|
||
|
||
elif market == "us":
|
||
indicator = "年报" if freq in ("annual", "yearly") else "单季报"
|
||
t = ticker.strip().upper().replace("-", ".")
|
||
df = ak.stock_financial_us_report_em(
|
||
stock=t, symbol="资产负债表", indicator=indicator,
|
||
)
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "balance sheet", ticker, freq)
|
||
|
||
else:
|
||
return f"Balance sheet not supported for HK stock {ticker} via akshare."
|
||
|
||
except Exception as e:
|
||
return f"[akshare] Error fetching balance sheet for {ticker}: {e}"
|
||
|
||
|
||
def get_cashflow(ticker: str, freq: str = "annual", curr_date: str = None) -> str:
|
||
"""Get cash flow statement. freq: 'annual' or 'quarterly'. Returns Markdown string."""
|
||
try:
|
||
market = _detect_market(ticker)
|
||
|
||
if market == "cn":
|
||
prefix = _get_exchange_prefix(ticker)
|
||
df = ak.stock_cash_flow_sheet_by_report_em(symbol=f"{prefix}{ticker.strip()}")
|
||
if freq in ("annual", "yearly"):
|
||
df = df[df["REPORT_TYPE"] == "年报"]
|
||
else:
|
||
df = df[df["REPORT_TYPE"].isin(["一季报", "中报", "三季报"])]
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "cash flow", ticker, freq)
|
||
|
||
elif market == "us":
|
||
indicator = "年报" if freq in ("annual", "yearly") else "单季报"
|
||
t = ticker.strip().upper().replace("-", ".")
|
||
df = ak.stock_financial_us_report_em(
|
||
stock=t, symbol="现金流量表", indicator=indicator,
|
||
)
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "cash flow", ticker, freq)
|
||
|
||
else:
|
||
return f"Cash flow statement not supported for HK stock {ticker} via akshare."
|
||
|
||
except Exception as e:
|
||
return f"[akshare] Error fetching cash flow for {ticker}: {e}"
|
||
|
||
|
||
def get_income_statement(ticker: str, freq: str = "annual", curr_date: str = None) -> str:
|
||
"""Get income statement. freq: 'annual' or 'quarterly'. Returns Markdown string."""
|
||
try:
|
||
market = _detect_market(ticker)
|
||
|
||
if market == "cn":
|
||
prefix = _get_exchange_prefix(ticker)
|
||
df = ak.stock_profit_sheet_by_report_em(symbol=f"{prefix}{ticker.strip()}")
|
||
if freq in ("annual", "yearly"):
|
||
df = df[df["REPORT_TYPE"] == "年报"]
|
||
else:
|
||
df = df[df["REPORT_TYPE"].isin(["一季报", "中报", "三季报"])]
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "income statement", ticker, freq)
|
||
|
||
elif market == "us":
|
||
indicator = "年报" if freq in ("annual", "yearly") else "单季报"
|
||
t = ticker.strip().upper().replace("-", ".")
|
||
df = ak.stock_financial_us_report_em(
|
||
stock=t, symbol="综合损益表", indicator=indicator,
|
||
)
|
||
if curr_date and not df.empty:
|
||
df = df[pd.to_datetime(df["REPORT_DATE"]) <= pd.Timestamp(curr_date)]
|
||
return _fmt_financial(df, "income statement", ticker, freq)
|
||
|
||
else:
|
||
return f"Income statement not supported for HK stock {ticker} via akshare."
|
||
|
||
except Exception as e:
|
||
return f"[akshare] Error fetching income statement for {ticker}: {e}"
|
||
|
||
|
||
def get_insider_transactions(ticker: str) -> str:
|
||
"""Insider transactions not supported by akshare.
|
||
Raise AlphaVantageRateLimitError to trigger fallback to yfinance."""
|
||
from .alpha_vantage_common import AlphaVantageRateLimitError
|
||
raise AlphaVantageRateLimitError(
|
||
f"akshare does not support insider transactions for {ticker}. "
|
||
"Falling back to next vendor."
|
||
)
|