add crypto instrument support and deepseek/kimi provider robustness

This commit is contained in:
liminghao 2026-04-02 03:04:12 +08:00
parent 9745421555
commit eb88674c1d
18 changed files with 376 additions and 65 deletions

View File

@ -161,6 +161,7 @@ tradingagents # installed command
python -m cli.main # alternative: run directly from source
```
You will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more.
Ticker input supports both equities and crypto symbols (for example: `SPY`, `7203.T`, `BTC-USD`, `ETH/USDT`, `BTCUSDT`).
<p align="center">
<img src="assets/cli/cli_init.png" width="100%" style="display: inline-block; margin: 0 2%;">
@ -195,6 +196,10 @@ ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy())
# forward propagate
_, decision = ta.propagate("NVDA", "2026-01-15")
print(decision)
# crypto example
_, crypto_decision = ta.propagate("BTC-USD", "2026-01-15")
print(crypto_decision)
```
You can also adjust the default configuration to set your own choice of LLMs, debate rounds, etc.

View File

@ -25,6 +25,7 @@ from rich.rule import Rule
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.instruments import get_asset_class
from cli.models import AnalystType
from cli.utils import *
from cli.announcements import fetch_announcements, display_announcements
@ -502,7 +503,7 @@ def get_user_selections():
console.print(
create_question_box(
"Step 1: Ticker Symbol",
"Enter the exact ticker symbol to analyze, including exchange suffix when needed (examples: SPY, CNC.TO, 7203.T, 0700.HK)",
"Enter the exact ticker symbol to analyze, including exchange suffix or crypto pair when needed (examples: SPY, CNC.TO, 7203.T, 0700.HK, BTC-USD, ETH/USDT)",
"SPY",
)
)
@ -613,7 +614,8 @@ def get_user_selections():
def get_ticker():
"""Get ticker symbol from user input."""
return typer.prompt("", default="SPY")
raw = typer.prompt("", default="SPY")
return normalize_ticker_symbol(raw)
def get_analysis_date():
@ -928,6 +930,7 @@ def format_tool_args(args, max_length=80) -> str:
def run_analysis():
# First get all user selections
selections = get_user_selections()
asset_class = get_asset_class(selections["ticker"])
# Create config with selected research depth
config = DEFAULT_CONFIG.copy()
@ -942,6 +945,7 @@ def run_analysis():
config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort")
config["anthropic_effort"] = selections.get("anthropic_effort")
config["output_language"] = selections.get("output_language", "English")
config["asset_class"] = asset_class
# Create stats callback handler for tracking LLM/tool calls
stats_handler = StatsCallbackHandler()
@ -1021,6 +1025,7 @@ def run_analysis():
# Add initial messages
message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}")
message_buffer.add_message("System", f"Detected asset class: {asset_class}")
message_buffer.add_message(
"System", f"Analysis date: {selections['analysis_date']}"
)

View File

@ -4,11 +4,11 @@ from typing import List, Optional, Tuple, Dict
from rich.console import Console
from cli.models import AnalystType
from tradingagents.llm_clients.model_catalog import get_model_options
from tradingagents.instruments import normalize_instrument_symbol
console = Console()
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK, BTC-USD, ETH/USDT"
ANALYST_ORDER = [
("Market Analyst", AnalystType.MARKET),
@ -39,8 +39,8 @@ def get_ticker() -> str:
def normalize_ticker_symbol(ticker: str) -> str:
"""Normalize ticker input while preserving exchange suffixes."""
return ticker.strip().upper()
"""Normalize symbol input for both equities and cryptocurrencies."""
return normalize_instrument_symbol(ticker)
def get_analysis_date() -> str:
@ -176,6 +176,7 @@ def select_openrouter_model() -> str:
def select_shallow_thinking_agent(provider) -> str:
"""Select shallow thinking llm engine using an interactive selection."""
from tradingagents.llm_clients.model_catalog import get_model_options
if provider.lower() == "openrouter":
return select_openrouter_model()
@ -207,6 +208,7 @@ def select_shallow_thinking_agent(provider) -> str:
def select_deep_thinking_agent(provider) -> str:
"""Select deep thinking llm engine using an interactive selection."""
from tradingagents.llm_clients.model_catalog import get_model_options
if provider.lower() == "openrouter":
return select_openrouter_model()

View File

@ -24,8 +24,8 @@ config["data_vendors"] = {
# Initialize with custom config
ta = TradingAgentsGraph(debug=True, config=config)
# forward propagate
_, decision = ta.propagate("NVDA", "2024-05-10")
# forward propagate (equity or crypto symbol)
_, decision = ta.propagate("BTC-USD", "2024-05-10")
print(decision)
# Memorize mistakes and reflect

34
tests/test_instruments.py Normal file
View File

@ -0,0 +1,34 @@
import unittest
from tradingagents.instruments import (
get_asset_class,
is_crypto_symbol,
normalize_instrument_symbol,
)
class InstrumentSymbolTests(unittest.TestCase):
def test_preserves_exchange_suffix(self):
self.assertEqual(normalize_instrument_symbol(" cnc.to "), "CNC.TO")
def test_normalizes_slash_pair(self):
self.assertEqual(normalize_instrument_symbol("eth/usdt"), "ETH-USD")
def test_normalizes_concat_pair(self):
self.assertEqual(normalize_instrument_symbol("BTCUSDT"), "BTC-USD")
def test_normalizes_bare_major_crypto(self):
self.assertEqual(normalize_instrument_symbol("btc"), "BTC-USD")
self.assertEqual(normalize_instrument_symbol("ont"), "ONT-USD")
def test_detects_crypto_asset_class(self):
self.assertTrue(is_crypto_symbol("ETH/USDT"))
self.assertEqual(get_asset_class("BTC"), "crypto")
def test_keeps_equity_asset_class(self):
self.assertFalse(is_crypto_symbol("AAPL"))
self.assertEqual(get_asset_class("AAPL"), "equity")
if __name__ == "__main__":
unittest.main()

View File

@ -26,6 +26,27 @@ class LLMProviderSupportTests(unittest.TestCase):
self.assertEqual(kwargs["api_key"], "deepseek-test-key")
self.assertEqual(kwargs["model"], "deepseek-chat")
@patch("tradingagents.llm_clients.openai_client.NormalizedChatOpenAI")
def test_deepseek_supports_custom_base_url(self, mock_chat_openai):
with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "deepseek-test-key"}, clear=False):
client = OpenAIClient(
"deepseek-chat",
provider="deepseek",
base_url="https://proxy.example.com/v1",
)
client.get_llm()
kwargs = mock_chat_openai.call_args.kwargs
self.assertEqual(kwargs["base_url"], "https://proxy.example.com/v1")
self.assertEqual(kwargs["api_key"], "deepseek-test-key")
self.assertEqual(kwargs["model"], "deepseek-chat")
def test_deepseek_missing_key_raises_clear_error(self):
with patch.dict(os.environ, {"DEEPSEEK_API_KEY": ""}, clear=False):
client = OpenAIClient("deepseek-chat", provider="deepseek")
with self.assertRaisesRegex(ValueError, "Missing API key for provider 'deepseek'"):
client.get_llm()
@patch("tradingagents.llm_clients.openai_client.NormalizedChatOpenAI")
def test_kimi_prefers_kimi_api_key(self, mock_chat_openai):
with patch.dict(

View File

@ -2,17 +2,42 @@ import unittest
from cli.utils import normalize_ticker_symbol
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.instruments import get_asset_class, is_crypto_symbol
class TickerSymbolHandlingTests(unittest.TestCase):
def test_normalize_ticker_symbol_preserves_exchange_suffix(self):
self.assertEqual(normalize_ticker_symbol(" cnc.to "), "CNC.TO")
def test_normalize_ticker_symbol_supports_crypto_slash_pair(self):
self.assertEqual(normalize_ticker_symbol("eth/usdt"), "ETH-USD")
def test_normalize_ticker_symbol_supports_crypto_concat_pair(self):
self.assertEqual(normalize_ticker_symbol("btcusdt"), "BTC-USD")
def test_normalize_ticker_symbol_supports_bare_crypto_base(self):
self.assertEqual(normalize_ticker_symbol("btc"), "BTC-USD")
self.assertEqual(normalize_ticker_symbol("ont"), "ONT-USD")
def test_build_instrument_context_mentions_exact_symbol(self):
context = build_instrument_context("7203.T")
self.assertIn("7203.T", context)
self.assertIn("exchange suffix", context)
def test_build_instrument_context_mentions_crypto_pair_rules(self):
context = build_instrument_context("BTC-USD")
self.assertIn("BTC-USD", context)
self.assertIn("cryptocurrency", context)
self.assertIn("24/7", context)
def test_get_asset_class_detects_crypto(self):
self.assertEqual(get_asset_class("BTC-USD"), "crypto")
self.assertTrue(is_crypto_symbol("ETH/USDT"))
def test_get_asset_class_defaults_to_equity(self):
self.assertEqual(get_asset_class("AAPL"), "equity")
self.assertFalse(is_crypto_symbol("AAPL"))
if __name__ == "__main__":
unittest.main()

View File

@ -1,5 +1,6 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from tradingagents.agents.utils.agent_utils import (
build_fundamentals_context,
build_instrument_context,
get_balance_sheet,
get_cashflow,
@ -14,7 +15,9 @@ from tradingagents.dataflows.config import get_config
def create_fundamentals_analyst(llm):
def fundamentals_analyst_node(state):
current_date = state["trade_date"]
instrument_context = build_instrument_context(state["company_of_interest"])
symbol = state["company_of_interest"]
instrument_context = build_instrument_context(symbol)
fundamentals_context = build_fundamentals_context(symbol)
tools = [
get_fundamentals,
@ -24,9 +27,12 @@ def create_fundamentals_analyst(llm):
]
system_message = (
"You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
"You are a researcher tasked with analyzing fundamental information about the target trading instrument. "
"Write a comprehensive report that helps traders understand intrinsic value drivers and key risks. "
"Use as much concrete evidence as possible."
+ " Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."
+ " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements."
+ " Use the available tools: `get_fundamentals` for overview data, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for financial statement detail when available."
+ f" {fundamentals_context}"
+ get_language_instruction(),
)

View File

@ -20,7 +20,7 @@ def create_market_analyst(llm):
]
system_message = (
"""You are a trading assistant tasked with analyzing financial markets. Your role is to select the **most relevant indicators** for a given market condition or trading strategy from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are:
"""You are a trading assistant tasked with analyzing financial markets for the target instrument. Your role is to select the **most relevant indicators** for a given market condition or trading strategy from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are:
Moving Averages:
- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend direction and serve as dynamic support/resistance. Tips: It lags price; combine with faster indicators for timely signals.
@ -44,7 +44,7 @@ Volatility Indicators:
Volume-Based Indicators:
- 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.
- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."""
- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the historical OHLCV CSV needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."""
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
+ get_language_instruction()
)

View File

@ -19,7 +19,10 @@ def create_news_analyst(llm):
]
system_message = (
"You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
"You are a news researcher tasked with analyzing recent news and trends over the past week. "
"Write a comprehensive report of world events that are relevant to the target instrument and macroeconomics. "
"Use the available tools: get_news(query, start_date, end_date) for instrument-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. "
"Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
+ get_language_instruction()
)

View File

@ -13,7 +13,10 @@ def create_social_media_analyst(llm):
]
system_message = (
"You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
"You are a social-media and sentiment analyst tasked with analyzing social posts, recent news, and public sentiment for a specific trading instrument over the past week. "
"Write a comprehensive report detailing your analysis, insights, and implications for traders and investors. "
"Use the get_news(query, start_date, end_date) tool to search for instrument-specific discussions and sentiment-relevant coverage. "
"Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
+ get_language_instruction()
)

View File

@ -19,11 +19,11 @@ def create_bear_researcher(llm, memory):
for i, rec in enumerate(past_memories, 1):
past_memory_str += rec["recommendation"] + "\n\n"
prompt = f"""You are a Bear Analyst making the case against investing in the stock. Your goal is to present a well-reasoned argument emphasizing risks, challenges, and negative indicators. Leverage the provided research and data to highlight potential downsides and counter bullish arguments effectively.
prompt = f"""You are a Bear Analyst making the case against investing in the target instrument. Your goal is to present a well-reasoned argument emphasizing risks, challenges, and negative indicators. Leverage the provided research and data to highlight potential downsides and counter bullish arguments effectively.
Key points to focus on:
- Risks and Challenges: Highlight factors like market saturation, financial instability, or macroeconomic threats that could hinder the stock's performance.
- Risks and Challenges: Highlight factors like market saturation, structural weaknesses, or macroeconomic threats that could hinder performance.
- Competitive Weaknesses: Emphasize vulnerabilities such as weaker market positioning, declining innovation, or threats from competitors.
- Negative Indicators: Use evidence from financial data, market trends, or recent adverse news to support your position.
- Bull Counterpoints: Critically analyze the bull argument with specific data and sound reasoning, exposing weaknesses or over-optimistic assumptions.
@ -34,11 +34,11 @@ Resources available:
Market research report: {market_research_report}
Social media sentiment report: {sentiment_report}
Latest world affairs news: {news_report}
Company fundamentals report: {fundamentals_report}
Fundamentals report: {fundamentals_report}
Conversation history of the debate: {history}
Last bull argument: {current_response}
Reflections from similar situations and lessons learned: {past_memory_str}
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of investing in the stock. You must also address reflections and learn from lessons and mistakes you made in the past.
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of taking exposure. You must also address reflections and learn from lessons and mistakes you made in the past.
"""
response = llm.invoke(prompt)

View File

@ -19,11 +19,11 @@ def create_bull_researcher(llm, memory):
for i, rec in enumerate(past_memories, 1):
past_memory_str += rec["recommendation"] + "\n\n"
prompt = f"""You are a Bull Analyst advocating for investing in the stock. Your task is to build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators. Leverage the provided research and data to address concerns and counter bearish arguments effectively.
prompt = f"""You are a Bull Analyst advocating for a long exposure to the target instrument. Your task is to build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators. Leverage the provided research and data to address concerns and counter bearish arguments effectively.
Key points to focus on:
- Growth Potential: Highlight the company's market opportunities, revenue projections, and scalability.
- Competitive Advantages: Emphasize factors like unique products, strong branding, or dominant market positioning.
- Growth Potential: Highlight major upside drivers, adoption trends, and scalability.
- Competitive Advantages: Emphasize factors like ecosystem strength, liquidity depth, or dominant market positioning.
- Positive Indicators: Use financial health, industry trends, and recent positive news as evidence.
- Bear Counterpoints: Critically analyze the bear argument with specific data and sound reasoning, addressing concerns thoroughly and showing why the bull perspective holds stronger merit.
- Engagement: Present your argument in a conversational style, engaging directly with the bear analyst's points and debating effectively rather than just listing data.
@ -32,7 +32,7 @@ Resources available:
Market research report: {market_research_report}
Social media sentiment report: {sentiment_report}
Latest world affairs news: {news_report}
Company fundamentals report: {fundamentals_report}
Fundamentals report: {fundamentals_report}
Conversation history of the debate: {history}
Last bear argument: {current_response}
Reflections from similar situations and lessons learned: {past_memory_str}

View File

@ -1,6 +1,7 @@
from langchain_core.messages import HumanMessage, RemoveMessage
# Import tools from separate utility files
from tradingagents.instruments import get_asset_class
from tradingagents.agents.utils.core_stock_tools import (
get_stock_data
)
@ -35,13 +36,36 @@ def get_language_instruction() -> str:
def build_instrument_context(ticker: str) -> str:
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
"""Describe the instrument format so agents preserve the exact symbol."""
asset_class = get_asset_class(ticker)
if asset_class == "crypto":
return (
f"The instrument to analyze is `{ticker}` (cryptocurrency). "
"Use this exact symbol in every tool call, report, and recommendation, "
"preserving the base/quote pair format (e.g. `BTC-USD`, `ETH-USD`). "
"Crypto markets trade 24/7, so do not assume weekends are closed."
)
return (
f"The instrument to analyze is `{ticker}`. "
"Use this exact ticker in every tool call, report, and recommendation, "
"preserving any exchange suffix (e.g. `.TO`, `.L`, `.HK`, `.T`)."
)
def build_fundamentals_context(ticker: str) -> str:
"""Return asset-aware guidance for the fundamentals analyst."""
if get_asset_class(ticker) == "crypto":
return (
"For cryptocurrency instruments, interpret fundamentals as tokenomics and market structure: "
"supply dynamics, liquidity, exchange coverage, adoption indicators, and macro/regulatory drivers. "
"If company financial statements or insider filings are unavailable, explicitly mark those sections as N/A."
)
return (
"For equities, prioritize corporate fundamentals such as business profile, profitability, "
"cash flow quality, leverage, and financial statement trends."
)
def create_msg_delete():
def delete_messages(state):
"""Clear messages and add placeholder for Anthropic compatibility"""

View File

@ -125,4 +125,4 @@ class StockstatsUtils:
indicator_value = matching_rows[indicator].values[0]
return indicator_value
else:
return "N/A: Not a trading day (weekend or holiday)"
return "N/A: No market data available for this date"

View File

@ -3,8 +3,9 @@ from datetime import datetime
from dateutil.relativedelta import relativedelta
import pandas as pd
import yfinance as yf
import os
import pandas as pd
from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date
from tradingagents.instruments import get_asset_class
def get_YFin_data_online(
symbol: Annotated[str, "ticker symbol of the company"],
@ -24,7 +25,7 @@ def get_YFin_data_online(
# Check if data is empty
if data.empty:
return (
f"No data found for symbol '{symbol}' between {start_date} and {end_date}"
f"No market data found for symbol '{symbol}' between {start_date} and {end_date}"
)
# Remove timezone info from index for cleaner output
@ -41,7 +42,7 @@ def get_YFin_data_online(
csv_string = data.to_csv()
# Add header information
header = f"# Stock data for {symbol.upper()} from {start_date} to {end_date}\n"
header = f"# Market data for {symbol.upper()} from {start_date} to {end_date}\n"
header += f"# Total records: {len(data)}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
@ -153,7 +154,7 @@ def get_stock_stats_indicators_window(
if date_str in indicator_data:
indicator_value = indicator_data[date_str]
else:
indicator_value = "N/A: Not a trading day (weekend or holiday)"
indicator_value = "N/A: No market data available for this date"
date_values.append((date_str, indicator_value))
current_dt = current_dt - relativedelta(days=1)
@ -249,7 +250,7 @@ def get_fundamentals(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date (not used for yfinance)"] = None
):
"""Get company fundamentals overview from yfinance."""
"""Get asset fundamentals overview from yfinance."""
try:
ticker_obj = yf.Ticker(ticker.upper())
info = yf_retry(lambda: ticker_obj.info)
@ -257,45 +258,69 @@ def get_fundamentals(
if not info:
return f"No fundamentals data found for symbol '{ticker}'"
fields = [
("Name", info.get("longName")),
("Sector", info.get("sector")),
("Industry", info.get("industry")),
("Market Cap", info.get("marketCap")),
("PE Ratio (TTM)", info.get("trailingPE")),
("Forward PE", info.get("forwardPE")),
("PEG Ratio", info.get("pegRatio")),
("Price to Book", info.get("priceToBook")),
("EPS (TTM)", info.get("trailingEps")),
("Forward EPS", info.get("forwardEps")),
("Dividend Yield", info.get("dividendYield")),
("Beta", info.get("beta")),
("52 Week High", info.get("fiftyTwoWeekHigh")),
("52 Week Low", info.get("fiftyTwoWeekLow")),
("50 Day Average", info.get("fiftyDayAverage")),
("200 Day Average", info.get("twoHundredDayAverage")),
("Revenue (TTM)", info.get("totalRevenue")),
("Gross Profit", info.get("grossProfits")),
("EBITDA", info.get("ebitda")),
("Net Income", info.get("netIncomeToCommon")),
("Profit Margin", info.get("profitMargins")),
("Operating Margin", info.get("operatingMargins")),
("Return on Equity", info.get("returnOnEquity")),
("Return on Assets", info.get("returnOnAssets")),
("Debt to Equity", info.get("debtToEquity")),
("Current Ratio", info.get("currentRatio")),
("Book Value", info.get("bookValue")),
("Free Cash Flow", info.get("freeCashflow")),
]
asset_class = get_asset_class(ticker)
if asset_class == "crypto":
fields = [
("Asset Name", info.get("longName") or info.get("shortName")),
("Symbol", ticker.upper()),
("Quote Currency", info.get("currency")),
("Market Cap", info.get("marketCap")),
("24h Volume", info.get("volume24Hr") or info.get("volume")),
("Circulating Supply", info.get("circulatingSupply")),
("Total Supply", info.get("totalSupply")),
("Max Supply", info.get("maxSupply")),
("52 Week High", info.get("fiftyTwoWeekHigh")),
("52 Week Low", info.get("fiftyTwoWeekLow")),
("50 Day Average", info.get("fiftyDayAverage")),
("200 Day Average", info.get("twoHundredDayAverage")),
]
header = f"# Crypto Fundamentals for {ticker.upper()}\n"
else:
fields = [
("Name", info.get("longName")),
("Sector", info.get("sector")),
("Industry", info.get("industry")),
("Market Cap", info.get("marketCap")),
("PE Ratio (TTM)", info.get("trailingPE")),
("Forward PE", info.get("forwardPE")),
("PEG Ratio", info.get("pegRatio")),
("Price to Book", info.get("priceToBook")),
("EPS (TTM)", info.get("trailingEps")),
("Forward EPS", info.get("forwardEps")),
("Dividend Yield", info.get("dividendYield")),
("Beta", info.get("beta")),
("52 Week High", info.get("fiftyTwoWeekHigh")),
("52 Week Low", info.get("fiftyTwoWeekLow")),
("50 Day Average", info.get("fiftyDayAverage")),
("200 Day Average", info.get("twoHundredDayAverage")),
("Revenue (TTM)", info.get("totalRevenue")),
("Gross Profit", info.get("grossProfits")),
("EBITDA", info.get("ebitda")),
("Net Income", info.get("netIncomeToCommon")),
("Profit Margin", info.get("profitMargins")),
("Operating Margin", info.get("operatingMargins")),
("Return on Equity", info.get("returnOnEquity")),
("Return on Assets", info.get("returnOnAssets")),
("Debt to Equity", info.get("debtToEquity")),
("Current Ratio", info.get("currentRatio")),
("Book Value", info.get("bookValue")),
("Free Cash Flow", info.get("freeCashflow")),
]
header = f"# Company Fundamentals for {ticker.upper()}\n"
lines = []
for label, value in fields:
if value is not None:
lines.append(f"{label}: {value}")
header = f"# Company Fundamentals for {ticker.upper()}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
if not lines:
return (
header
+ "No structured fundamentals were returned by yfinance for this symbol.\n"
)
return header + "\n".join(lines)
except Exception as e:
@ -308,6 +333,10 @@ def get_balance_sheet(
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get balance sheet data from yfinance."""
if get_asset_class(ticker) == "crypto":
return (
f"N/A for cryptocurrency symbol '{ticker}': balance sheet is only available for companies."
)
try:
ticker_obj = yf.Ticker(ticker.upper())
@ -340,6 +369,10 @@ def get_cashflow(
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get cash flow data from yfinance."""
if get_asset_class(ticker) == "crypto":
return (
f"N/A for cryptocurrency symbol '{ticker}': cash flow statement is only available for companies."
)
try:
ticker_obj = yf.Ticker(ticker.upper())
@ -372,6 +405,10 @@ def get_income_statement(
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get income statement data from yfinance."""
if get_asset_class(ticker) == "crypto":
return (
f"N/A for cryptocurrency symbol '{ticker}': income statement is only available for companies."
)
try:
ticker_obj = yf.Ticker(ticker.upper())
@ -402,6 +439,10 @@ def get_insider_transactions(
ticker: Annotated[str, "ticker symbol of the company"]
):
"""Get insider transactions data from yfinance."""
if get_asset_class(ticker) == "crypto":
return (
f"N/A for cryptocurrency symbol '{ticker}': insider transactions are not applicable."
)
try:
ticker_obj = yf.Ticker(ticker.upper())
data = yf_retry(lambda: ticker_obj.insider_transactions)
@ -419,4 +460,4 @@ def get_insider_transactions(
return header + csv_string
except Exception as e:
return f"Error retrieving insider transactions for {ticker}: {str(e)}"
return f"Error retrieving insider transactions for {ticker}: {str(e)}"

View File

@ -0,0 +1,132 @@
import re
# Quotes commonly used for crypto trading pairs.
CRYPTO_QUOTES = {
"USD",
"USDT",
"USDC",
"BUSD",
"DAI",
"FDUSD",
"TUSD",
"BTC",
"ETH",
"BNB",
"EUR",
"JPY",
}
# Stablecoin quotes are normalized to USD for Yahoo Finance compatibility.
STABLECOIN_QUOTES = {"USDT", "USDC", "BUSD", "DAI", "FDUSD", "TUSD"}
# Popular crypto symbols used for auto-normalization when a quote is omitted.
MAJOR_CRYPTO_BASES = {
"BTC",
"ETH",
"ONT",
"SOL",
"XRP",
"BNB",
"DOGE",
"ADA",
"TRX",
"AVAX",
"DOT",
"MATIC",
"LTC",
"BCH",
"LINK",
"ATOM",
"UNI",
"AAVE",
"ETC",
"XLM",
"NEAR",
"FIL",
}
CONCAT_QUOTE_SUFFIXES = tuple(
sorted(
CRYPTO_QUOTES,
key=len,
reverse=True,
)
)
EXCHANGE_SUFFIX_PATTERN = re.compile(r"^[A-Z0-9]+(?:\.[A-Z0-9]{1,5})+$")
def _normalize_quote(quote: str) -> str:
if quote in STABLECOIN_QUOTES:
return "USD"
return quote
def normalize_instrument_symbol(symbol: str) -> str:
"""Normalize ticker-like input while preserving equity suffixes and crypto pairs.
Examples:
- " cnc.to " -> "CNC.TO"
- "btc-usdt" -> "BTC-USD"
- "eth/usdt" -> "ETH-USD"
- "BTCUSDT" -> "BTC-USD"
- "btc" -> "BTC-USD"
"""
normalized = symbol.strip().upper().replace(" ", "")
if not normalized:
return normalized
# TradingView-like venue prefixes (e.g. BINANCE:BTCUSDT).
if ":" in normalized and normalized.count(":") == 1:
_, normalized = normalized.split(":", 1)
# Preserve exchange-qualified equity symbols such as 7203.T or CNC.TO.
if EXCHANGE_SUFFIX_PATTERN.match(normalized):
return normalized
# Pair formats: BTC/USDT, BTC_USDT, BTC-USD.
pair_candidate = normalized.replace("_", "/")
if pair_candidate.count("/") == 1:
base, quote = pair_candidate.split("/")
if base.isalnum() and quote.isalnum():
return f"{base}-{_normalize_quote(quote)}"
if normalized.count("-") == 1:
base, quote = normalized.split("-")
if base.isalnum() and quote.isalnum():
return f"{base}-{_normalize_quote(quote)}"
# Concatenated pair format: BTCUSDT, ETHUSD, SOLBTC.
for suffix in CONCAT_QUOTE_SUFFIXES:
if normalized.endswith(suffix) and len(normalized) > len(suffix) + 1:
base = normalized[: -len(suffix)]
if base.isalnum():
return f"{base}-{_normalize_quote(suffix)}"
# Bare major crypto symbols default to USD quote.
if normalized in MAJOR_CRYPTO_BASES:
return f"{normalized}-USD"
return normalized
def is_crypto_symbol(symbol: str) -> bool:
"""Heuristic crypto detector based on normalized pair semantics."""
normalized = normalize_instrument_symbol(symbol)
if not normalized:
return False
if normalized.endswith("-USD") and normalized[:-4] in MAJOR_CRYPTO_BASES:
return True
if normalized.count("-") == 1 and "." not in normalized:
base, quote = normalized.split("-")
if base.isalnum() and quote in CRYPTO_QUOTES:
return True
return False
def get_asset_class(symbol: str) -> str:
"""Return 'crypto' for cryptocurrency symbols, otherwise 'equity'."""
return "crypto" if is_crypto_symbol(symbol) else "equity"

View File

@ -60,14 +60,24 @@ class OpenAIClient(BaseLLMClient):
# Provider-specific base URL and auth
if self.provider in _PROVIDER_CONFIG:
base_url, api_key_envs = _PROVIDER_CONFIG[self.provider]
llm_kwargs["base_url"] = base_url
default_base_url, api_key_envs = _PROVIDER_CONFIG[self.provider]
llm_kwargs["base_url"] = self.base_url or default_base_url
if api_key_envs:
resolved_api_key = self.kwargs.get("api_key")
for api_key_env in api_key_envs:
if resolved_api_key:
break
api_key = os.environ.get(api_key_env)
if api_key:
llm_kwargs["api_key"] = api_key
resolved_api_key = api_key
break
if not resolved_api_key:
api_key_env_list = ", ".join(api_key_envs)
raise ValueError(
f"Missing API key for provider '{self.provider}'. "
f"Set one of: {api_key_env_list}, or pass api_key explicitly."
)
llm_kwargs["api_key"] = resolved_api_key
else:
llm_kwargs["api_key"] = "ollama"
elif self.base_url: