Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
|
|
@ -1,15 +0,0 @@
|
|||
.git
|
||||
.venv
|
||||
.env
|
||||
.claude
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
__pycache__
|
||||
*.egg-info
|
||||
build
|
||||
dist
|
||||
results
|
||||
eval_results
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Azure OpenAI
|
||||
AZURE_OPENAI_API_KEY=
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME=
|
||||
# OPENAI_API_VERSION=2024-10-21 # optional, required for non-v1 API
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# LLM Providers (set the one you use)
|
||||
OPENAI_API_KEY=
|
||||
GOOGLE_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
XAI_API_KEY=
|
||||
DEEPSEEK_API_KEY=
|
||||
DASHSCOPE_API_KEY=
|
||||
ZHIPU_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
|
@ -1,219 +1,8 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# Cache
|
||||
**/data_cache/
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
*.csv
|
||||
src/
|
||||
eval_results/
|
||||
eval_data/
|
||||
*.egg-info/
|
||||
|
|
|
|||
27
Dockerfile
27
Dockerfile
|
|
@ -1,27 +0,0 @@
|
|||
FROM python:3.12-slim AS builder
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN useradd --create-home appuser
|
||||
USER appuser
|
||||
WORKDIR /home/appuser/app
|
||||
|
||||
COPY --from=builder --chown=appuser:appuser /build .
|
||||
|
||||
ENTRYPOINT ["tradingagents"]
|
||||
104
README.md
104
README.md
|
|
@ -11,37 +11,9 @@
|
|||
<a href="https://github.com/TauricResearch/" target="_blank"><img alt="Community" src="https://img.shields.io/badge/Join_GitHub_Community-TauricResearch-14C290?logo=discourse"/></a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=de">Deutsch</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=es">Español</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=fr">français</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ja">日本語</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ko">한국어</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=pt">Português</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ru">Русский</a> |
|
||||
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=zh">中文</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
# TradingAgents: Multi-Agents LLM Financial Trading Framework
|
||||
|
||||
## News
|
||||
- [2026-03] **TradingAgents v0.2.3** released with multi-language support, GPT-5.4 family models, unified model catalog, backtesting date fidelity, and proxy support.
|
||||
- [2026-03] **TradingAgents v0.2.2** released with GPT-5.4/Gemini 3.1/Claude 4.6 model coverage, five-tier rating scale, OpenAI Responses API, Anthropic effort control, and cross-platform stability.
|
||||
- [2026-02] **TradingAgents v0.2.0** released with multi-provider LLM support (GPT-5.x, Gemini 3.x, Claude 4.x, Grok 4.x) and improved system architecture.
|
||||
- [2026-01] **Trading-R1** [Technical Report](https://arxiv.org/abs/2509.11420) released, with [Terminal](https://github.com/TauricResearch/Trading-R1) expected to land soon.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.star-history.com/#TauricResearch/TradingAgents&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date" />
|
||||
<img alt="TradingAgents Star History" src="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date" style="width: 80%; height: auto;" />
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
# TradingAgents: Multi-Agents LLM Financial Trading Framework
|
||||
|
||||
> 🎉 **TradingAgents** officially released! We have received numerous inquiries about the work, and we would like to express our thanks for the enthusiasm in our community.
|
||||
>
|
||||
|
|
@ -86,7 +58,7 @@ Our framework decomposes complex trading tasks into specialized roles. This ensu
|
|||
- Composes reports from the analysts and researchers to make informed trading decisions. It determines the timing and magnitude of trades based on comprehensive market insights.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/trader.png" width="70%" style="display: inline-block; margin: 0 2%;">
|
||||
<img src="assets/risk.png" width="70%" style="display: inline-block; margin: 0 2%;">
|
||||
</p>
|
||||
|
||||
### Risk Management and Portfolio Manager
|
||||
|
|
@ -94,7 +66,7 @@ Our framework decomposes complex trading tasks into specialized roles. This ensu
|
|||
- The Portfolio Manager approves/rejects the transaction proposal. If approved, the order will be sent to the simulated exchange and executed.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/risk.png" width="70%" style="display: inline-block; margin: 0 2%;">
|
||||
<img src="assets/trader.png" width="70%" style="display: inline-block; margin: 0 2%;">
|
||||
</p>
|
||||
|
||||
## Installation and CLI
|
||||
|
|
@ -113,57 +85,30 @@ conda create -n tradingagents python=3.13
|
|||
conda activate tradingagents
|
||||
```
|
||||
|
||||
Install the package and its dependencies:
|
||||
Install dependencies:
|
||||
```bash
|
||||
pip install .
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Alternatively, run with Docker:
|
||||
```bash
|
||||
cp .env.example .env # add your API keys
|
||||
docker compose run --rm tradingagents
|
||||
```
|
||||
|
||||
For local models with Ollama:
|
||||
```bash
|
||||
docker compose --profile ollama run --rm tradingagents-ollama
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Required APIs
|
||||
|
||||
TradingAgents supports multiple LLM providers. Set the API key for your chosen provider:
|
||||
|
||||
You will also need the FinnHub API and EODHD API for financial data. All of our code is implemented with the free tier.
|
||||
```bash
|
||||
export OPENAI_API_KEY=... # OpenAI (GPT)
|
||||
export GOOGLE_API_KEY=... # Google (Gemini)
|
||||
export ANTHROPIC_API_KEY=... # Anthropic (Claude)
|
||||
export XAI_API_KEY=... # xAI (Grok)
|
||||
export DEEPSEEK_API_KEY=... # DeepSeek
|
||||
export DASHSCOPE_API_KEY=... # Qwen (Alibaba DashScope)
|
||||
export ZHIPU_API_KEY=... # GLM (Zhipu)
|
||||
export OPENROUTER_API_KEY=... # OpenRouter
|
||||
export ALPHA_VANTAGE_API_KEY=... # Alpha Vantage
|
||||
export FINNHUB_API_KEY=$YOUR_FINNHUB_API_KEY
|
||||
```
|
||||
|
||||
For enterprise providers (e.g. Azure OpenAI, AWS Bedrock), copy `.env.enterprise.example` to `.env.enterprise` and fill in your credentials.
|
||||
|
||||
For local models, configure Ollama with `llm_provider: "ollama"` in your config.
|
||||
|
||||
Alternatively, copy `.env.example` to `.env` and fill in your keys:
|
||||
You will need the OpenAI API for all the agents.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
export OPENAI_API_KEY=$YOUR_OPENAI_API_KEY
|
||||
```
|
||||
|
||||
### CLI Usage
|
||||
|
||||
Launch the interactive CLI:
|
||||
You can also try out the CLI directly by running:
|
||||
```bash
|
||||
tradingagents # installed command
|
||||
python -m cli.main # alternative: run directly from source
|
||||
python -m cli.main
|
||||
```
|
||||
You will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more.
|
||||
You will see a screen where you can select your desired tickers, date, LLMs, research depth, etc.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/cli/cli_init.png" width="100%" style="display: inline-block; margin: 0 2%;">
|
||||
|
|
@ -183,7 +128,7 @@ An interface will appear showing results as they load, letting you track the age
|
|||
|
||||
### Implementation Details
|
||||
|
||||
We built TradingAgents with LangGraph to ensure flexibility and modularity. The framework supports multiple LLM providers: OpenAI, Google, Anthropic, xAI, OpenRouter, and Ollama.
|
||||
We built TradingAgents with LangGraph to ensure flexibility and modularity. We utilize `o1-preview` and `gpt-4o` as our deep thinking and fast thinking LLMs for our experiments. However, for testing purposes, we recommend you use `o4-mini` and `gpt-4.1-mini` to save on costs as our framework makes **lots of** API calls.
|
||||
|
||||
### Python Usage
|
||||
|
||||
|
|
@ -191,12 +136,11 @@ To use TradingAgents inside your code, you can import the `tradingagents` module
|
|||
|
||||
```python
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy())
|
||||
ta = TradingAgentsGraph(debug=True, config=config)
|
||||
|
||||
# forward propagate
|
||||
_, decision = ta.propagate("NVDA", "2026-01-15")
|
||||
_, decision = ta.propagate("NVDA", "2024-05-10")
|
||||
print(decision)
|
||||
```
|
||||
|
||||
|
|
@ -206,18 +150,24 @@ You can also adjust the default configuration to set your own choice of LLMs, de
|
|||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
# Create a custom config
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["llm_provider"] = "openai" # openai, google, anthropic, xai, openrouter, ollama
|
||||
config["deep_think_llm"] = "gpt-5.4" # Model for complex reasoning
|
||||
config["quick_think_llm"] = "gpt-5.4-mini" # Model for quick tasks
|
||||
config["max_debate_rounds"] = 2
|
||||
config["deep_think_llm"] = "gpt-4.1-nano" # Use a different model
|
||||
config["quick_think_llm"] = "gpt-4.1-nano" # Use a different model
|
||||
config["max_debate_rounds"] = 1 # Increase debate rounds
|
||||
config["online_tools"] = True # Use online tools or cached data
|
||||
|
||||
# Initialize with custom config
|
||||
ta = TradingAgentsGraph(debug=True, config=config)
|
||||
_, decision = ta.propagate("NVDA", "2026-01-15")
|
||||
|
||||
# forward propagate
|
||||
_, decision = ta.propagate("NVDA", "2024-05-10")
|
||||
print(decision)
|
||||
```
|
||||
|
||||
See `tradingagents/default_config.py` for all configuration options.
|
||||
> For `online_tools`, we recommend enabling them for experimentation, as they provide access to real-time data. The agents' offline tools rely on cached data from our **Tauric TradingDB**, a curated dataset we use for backtesting. We're currently in the process of refining this dataset, and we plan to release it soon alongside our upcoming projects. Stay tuned!
|
||||
|
||||
You can view the full list of configurations in `tradingagents/default_config.py`.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
import getpass
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cli.config import CLI_CONFIG
|
||||
|
||||
|
||||
def fetch_announcements(url: str = None, timeout: float = None) -> dict:
|
||||
"""Fetch announcements from endpoint. Returns dict with announcements and settings."""
|
||||
endpoint = url or CLI_CONFIG["announcements_url"]
|
||||
timeout = timeout or CLI_CONFIG["announcements_timeout"]
|
||||
fallback = CLI_CONFIG["announcements_fallback"]
|
||||
|
||||
try:
|
||||
response = requests.get(endpoint, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return {
|
||||
"announcements": data.get("announcements", [fallback]),
|
||||
"require_attention": data.get("require_attention", False),
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"announcements": [fallback],
|
||||
"require_attention": False,
|
||||
}
|
||||
|
||||
|
||||
def display_announcements(console: Console, data: dict) -> None:
|
||||
"""Display announcements panel. Prompts for Enter if require_attention is True."""
|
||||
announcements = data.get("announcements", [])
|
||||
require_attention = data.get("require_attention", False)
|
||||
|
||||
if not announcements:
|
||||
return
|
||||
|
||||
content = "\n".join(announcements)
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
title="Announcements",
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
if require_attention:
|
||||
getpass.getpass("Press Enter to continue...")
|
||||
else:
|
||||
console.print()
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
CLI_CONFIG = {
|
||||
# Announcements
|
||||
"announcements_url": "https://api.tauric.ai/v1/announcements",
|
||||
"announcements_timeout": 1.0,
|
||||
"announcements_fallback": "[cyan]For more information, please visit[/cyan] [link=https://github.com/TauricResearch]https://github.com/TauricResearch[/link]",
|
||||
}
|
||||
1210
cli/main.py
1210
cli/main.py
File diff suppressed because it is too large
Load Diff
|
|
@ -1,76 +0,0 @@
|
|||
import threading
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from langchain_core.callbacks import BaseCallbackHandler
|
||||
from langchain_core.outputs import LLMResult
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
|
||||
class StatsCallbackHandler(BaseCallbackHandler):
|
||||
"""Callback handler that tracks LLM calls, tool calls, and token usage."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._lock = threading.Lock()
|
||||
self.llm_calls = 0
|
||||
self.tool_calls = 0
|
||||
self.tokens_in = 0
|
||||
self.tokens_out = 0
|
||||
|
||||
def on_llm_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
prompts: List[str],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment LLM call counter when an LLM starts."""
|
||||
with self._lock:
|
||||
self.llm_calls += 1
|
||||
|
||||
def on_chat_model_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
messages: List[List[Any]],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment LLM call counter when a chat model starts."""
|
||||
with self._lock:
|
||||
self.llm_calls += 1
|
||||
|
||||
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
|
||||
"""Extract token usage from LLM response."""
|
||||
try:
|
||||
generation = response.generations[0][0]
|
||||
except (IndexError, TypeError):
|
||||
return
|
||||
|
||||
usage_metadata = None
|
||||
if hasattr(generation, "message"):
|
||||
message = generation.message
|
||||
if isinstance(message, AIMessage) and hasattr(message, "usage_metadata"):
|
||||
usage_metadata = message.usage_metadata
|
||||
|
||||
if usage_metadata:
|
||||
with self._lock:
|
||||
self.tokens_in += usage_metadata.get("input_tokens", 0)
|
||||
self.tokens_out += usage_metadata.get("output_tokens", 0)
|
||||
|
||||
def on_tool_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
input_str: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment tool call counter when a tool starts."""
|
||||
with self._lock:
|
||||
self.tool_calls += 1
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Return current statistics."""
|
||||
with self._lock:
|
||||
return {
|
||||
"llm_calls": self.llm_calls,
|
||||
"tool_calls": self.tool_calls,
|
||||
"tokens_in": self.tokens_in,
|
||||
"tokens_out": self.tokens_out,
|
||||
}
|
||||
225
cli/utils.py
225
cli/utils.py
|
|
@ -1,14 +1,7 @@
|
|||
import questionary
|
||||
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
|
||||
|
||||
console = Console()
|
||||
|
||||
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
|
||||
|
||||
ANALYST_ORDER = [
|
||||
("Market Analyst", AnalystType.MARKET),
|
||||
|
|
@ -21,7 +14,7 @@ ANALYST_ORDER = [
|
|||
def get_ticker() -> str:
|
||||
"""Prompt the user to enter a ticker symbol."""
|
||||
ticker = questionary.text(
|
||||
f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):",
|
||||
"Enter the ticker symbol to analyze:",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
|
|
@ -35,11 +28,6 @@ def get_ticker() -> str:
|
|||
console.print("\n[red]No ticker symbol provided. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return normalize_ticker_symbol(ticker)
|
||||
|
||||
|
||||
def normalize_ticker_symbol(ticker: str) -> str:
|
||||
"""Normalize ticker input while preserving exchange suffixes."""
|
||||
return ticker.strip().upper()
|
||||
|
||||
|
||||
|
|
@ -134,70 +122,22 @@ def select_research_depth() -> int:
|
|||
return choice
|
||||
|
||||
|
||||
def _fetch_openrouter_models() -> List[Tuple[str, str]]:
|
||||
"""Fetch available models from the OpenRouter API."""
|
||||
import requests
|
||||
try:
|
||||
resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10)
|
||||
resp.raise_for_status()
|
||||
models = resp.json().get("data", [])
|
||||
return [(m.get("name") or m["id"], m["id"]) for m in models]
|
||||
except Exception as e:
|
||||
console.print(f"\n[yellow]Could not fetch OpenRouter models: {e}[/yellow]")
|
||||
return []
|
||||
def select_shallow_thinking_agent() -> str:
|
||||
"""Select shallow thinking llm engine using an interactive selection."""
|
||||
|
||||
|
||||
def select_openrouter_model() -> str:
|
||||
"""Select an OpenRouter model from the newest available, or enter a custom ID."""
|
||||
models = _fetch_openrouter_models()
|
||||
|
||||
choices = [questionary.Choice(name, value=mid) for name, mid in models[:5]]
|
||||
choices.append(questionary.Choice("Custom model ID", value="custom"))
|
||||
# Define shallow thinking llm engine options with their corresponding model names
|
||||
SHALLOW_AGENT_OPTIONS = [
|
||||
("GPT-4o-mini - Fast and efficient for quick tasks", "gpt-4o-mini"),
|
||||
("GPT-4.1-nano - Ultra-lightweight model for basic operations", "gpt-4.1-nano"),
|
||||
("GPT-4.1-mini - Compact model with good performance", "gpt-4.1-mini"),
|
||||
("GPT-4o - Standard model with solid capabilities", "gpt-4o"),
|
||||
]
|
||||
|
||||
choice = questionary.select(
|
||||
"Select OpenRouter Model (latest available):",
|
||||
choices=choices,
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style([
|
||||
("selected", "fg:magenta noinherit"),
|
||||
("highlighted", "fg:magenta noinherit"),
|
||||
("pointer", "fg:magenta noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
if choice is None or choice == "custom":
|
||||
return questionary.text(
|
||||
"Enter OpenRouter model ID (e.g. google/gemma-4-26b-a4b-it):",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a model ID.",
|
||||
).ask().strip()
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def _prompt_custom_model_id() -> str:
|
||||
"""Prompt user to type a custom model ID."""
|
||||
return questionary.text(
|
||||
"Enter model ID:",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a model ID.",
|
||||
).ask().strip()
|
||||
|
||||
|
||||
def _select_model(provider: str, mode: str) -> str:
|
||||
"""Select a model for the given provider and mode (quick/deep)."""
|
||||
if provider.lower() == "openrouter":
|
||||
return select_openrouter_model()
|
||||
|
||||
if provider.lower() == "azure":
|
||||
return questionary.text(
|
||||
f"Enter Azure deployment name ({mode}-thinking):",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a deployment name.",
|
||||
).ask().strip()
|
||||
|
||||
choice = questionary.select(
|
||||
f"Select Your [{mode.title()}-Thinking LLM Engine]:",
|
||||
"Select Your [Quick-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in get_model_options(provider, mode)
|
||||
for display, value in SHALLOW_AGENT_OPTIONS
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
|
|
@ -210,45 +150,33 @@ def _select_model(provider: str, mode: str) -> str:
|
|||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print(f"\n[red]No {mode} thinking llm engine selected. Exiting...[/red]")
|
||||
console.print(
|
||||
"\n[red]No shallow thinking llm engine selected. Exiting...[/red]"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if choice == "custom":
|
||||
return _prompt_custom_model_id()
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def select_shallow_thinking_agent(provider) -> str:
|
||||
"""Select shallow thinking llm engine using an interactive selection."""
|
||||
return _select_model(provider, "quick")
|
||||
|
||||
|
||||
def select_deep_thinking_agent(provider) -> str:
|
||||
def select_deep_thinking_agent() -> str:
|
||||
"""Select deep thinking llm engine using an interactive selection."""
|
||||
return _select_model(provider, "deep")
|
||||
|
||||
def select_llm_provider() -> tuple[str, str | None]:
|
||||
"""Select the LLM provider and its API endpoint."""
|
||||
# (display_name, provider_key, base_url)
|
||||
PROVIDERS = [
|
||||
("OpenAI", "openai", "https://api.openai.com/v1"),
|
||||
("Google", "google", None),
|
||||
("Anthropic", "anthropic", "https://api.anthropic.com/"),
|
||||
("xAI", "xai", "https://api.x.ai/v1"),
|
||||
("DeepSeek", "deepseek", "https://api.deepseek.com"),
|
||||
("Qwen", "qwen", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
||||
("GLM", "glm", "https://open.bigmodel.cn/api/paas/v4/"),
|
||||
("OpenRouter", "openrouter", "https://openrouter.ai/api/v1"),
|
||||
("Azure OpenAI", "azure", None),
|
||||
("Ollama", "ollama", "http://localhost:11434/v1"),
|
||||
# Define deep thinking llm engine options with their corresponding model names
|
||||
DEEP_AGENT_OPTIONS = [
|
||||
("GPT-4.1-nano - Ultra-lightweight model for basic operations", "gpt-4.1-nano"),
|
||||
("GPT-4.1-mini - Compact model with good performance", "gpt-4.1-mini"),
|
||||
("GPT-4o - Standard model with solid capabilities", "gpt-4o"),
|
||||
("o4-mini - Specialized reasoning model (compact)", "o4-mini"),
|
||||
("o3-mini - Advanced reasoning model (lightweight)", "o3-mini"),
|
||||
("o3 - Full advanced reasoning model", "o3"),
|
||||
("o1 - Premier reasoning and problem-solving model", "o1"),
|
||||
]
|
||||
|
||||
choice = questionary.select(
|
||||
"Select your LLM Provider:",
|
||||
"Select Your [Deep-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=(provider_key, url))
|
||||
for display, provider_key, url in PROVIDERS
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in DEEP_AGENT_OPTIONS
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
|
|
@ -259,102 +187,9 @@ def select_llm_provider() -> tuple[str, str | None]:
|
|||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]No LLM provider selected. Exiting...[/red]")
|
||||
console.print("\n[red]No deep thinking llm engine selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
provider, url = choice
|
||||
return provider, url
|
||||
|
||||
|
||||
def ask_openai_reasoning_effort() -> str:
|
||||
"""Ask for OpenAI reasoning effort level."""
|
||||
choices = [
|
||||
questionary.Choice("Medium (Default)", "medium"),
|
||||
questionary.Choice("High (More thorough)", "high"),
|
||||
questionary.Choice("Low (Faster)", "low"),
|
||||
]
|
||||
return questionary.select(
|
||||
"Select Reasoning Effort:",
|
||||
choices=choices,
|
||||
style=questionary.Style([
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_anthropic_effort() -> str | None:
|
||||
"""Ask for Anthropic effort level.
|
||||
|
||||
Controls token usage and response thoroughness on Claude 4.5+ and 4.6 models.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select Effort Level:",
|
||||
choices=[
|
||||
questionary.Choice("High (recommended)", "high"),
|
||||
questionary.Choice("Medium (balanced)", "medium"),
|
||||
questionary.Choice("Low (faster, cheaper)", "low"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_gemini_thinking_config() -> str | None:
|
||||
"""Ask for Gemini thinking configuration.
|
||||
|
||||
Returns thinking_level: "high" or "minimal".
|
||||
Client maps to appropriate API param based on model series.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select Thinking Mode:",
|
||||
choices=[
|
||||
questionary.Choice("Enable Thinking (recommended)", "high"),
|
||||
questionary.Choice("Minimal/Disable Thinking", "minimal"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:green noinherit"),
|
||||
("highlighted", "fg:green noinherit"),
|
||||
("pointer", "fg:green noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_output_language() -> str:
|
||||
"""Ask for report output language."""
|
||||
choice = questionary.select(
|
||||
"Select Output Language:",
|
||||
choices=[
|
||||
questionary.Choice("English (default)", "English"),
|
||||
questionary.Choice("Chinese (中文)", "Chinese"),
|
||||
questionary.Choice("Japanese (日本語)", "Japanese"),
|
||||
questionary.Choice("Korean (한국어)", "Korean"),
|
||||
questionary.Choice("Hindi (हिन्दी)", "Hindi"),
|
||||
questionary.Choice("Spanish (Español)", "Spanish"),
|
||||
questionary.Choice("Portuguese (Português)", "Portuguese"),
|
||||
questionary.Choice("French (Français)", "French"),
|
||||
questionary.Choice("German (Deutsch)", "German"),
|
||||
questionary.Choice("Arabic (العربية)", "Arabic"),
|
||||
questionary.Choice("Russian (Русский)", "Russian"),
|
||||
questionary.Choice("Custom language", "custom"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:yellow noinherit"),
|
||||
("highlighted", "fg:yellow noinherit"),
|
||||
("pointer", "fg:yellow noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
if choice == "custom":
|
||||
return questionary.text(
|
||||
"Enter language name (e.g. Turkish, Vietnamese, Thai, Indonesian):",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a language name.",
|
||||
).ask().strip()
|
||||
|
||||
return choice
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
services:
|
||||
tradingagents:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- tradingagents_data:/home/appuser/.tradingagents
|
||||
tty: true
|
||||
stdin_open: true
|
||||
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
profiles:
|
||||
- ollama
|
||||
|
||||
tradingagents-ollama:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- LLM_PROVIDER=ollama
|
||||
volumes:
|
||||
- tradingagents_data:/home/appuser/.tradingagents
|
||||
depends_on:
|
||||
- ollama
|
||||
tty: true
|
||||
stdin_open: true
|
||||
profiles:
|
||||
- ollama
|
||||
|
||||
volumes:
|
||||
tradingagents_data:
|
||||
ollama_data:
|
||||
|
|
@ -0,0 +1,447 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="TradingAgents: Multi-Agents LLM Financial Trading Framework">
|
||||
<meta name="keywords" content="TradingAgents, LLM, Financial Trading, Multi-Agent Systems, Financial Markets, AI Trading">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TradingAgents: Multi-Agents LLM Financial Trading Framework</title>
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PYVRSFMDRL"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-PYVRSFMDRL');
|
||||
</script>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Google+Sans|Noto+Sans|Castoro" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./static/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="./static/css/bulma-carousel.min.css">
|
||||
<link rel="stylesheet" href="./static/css/bulma-slider.min.css">
|
||||
<link rel="stylesheet" href="./static/css/fontawesome.all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jpswalsh/academicons@1/css/academicons.min.css">
|
||||
<link rel="stylesheet" href="./static/css/index.css">
|
||||
<link rel="icon" href="./static/images/TradingAgents.png">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script defer src="./static/js/fontawesome.all.min.js"></script>
|
||||
<script src="./static/js/bulma-carousel.min.js"></script>
|
||||
<script src="./static/js/bulma-slider.min.js"></script>
|
||||
<script src="./static/js/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start" style="flex-grow: 1; justify-content: center;">
|
||||
<a class="navbar-item" href="https://yijia-xiao.github.io/">
|
||||
<span class="icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</span>
|
||||
</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">More Research</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="https://arxiv.org/abs/2408.11363">ProteinGPT</a>
|
||||
<a class="navbar-item" href="https://arxiv.org/abs/2310.02469">PrivacyMind</a>
|
||||
<a class="navbar-item" href="https://arxiv.org/abs/2412.20138">TradingAgents</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column has-text-centered">
|
||||
<h1 class="title is-1 publication-title">TradingAgents: Multi-Agents LLM Financial Trading Framework</h1>
|
||||
<div class="is-size-5 publication-authors">
|
||||
<a href="https://yijia-xiao.github.io/" target="_blank" rel="noopener noreferrer"><span class="author-block">Yijia Xiao<sup>1</sup>,</span></a>
|
||||
<span class="author-block">Edward Sun<sup>1</sup>,</span>
|
||||
<span class="author-block">Di Luo<sup>1,2</sup>,</span>
|
||||
<span class="author-block">Wei Wang<sup>1</sup></span>
|
||||
</div>
|
||||
<div class="is-size-5 publication-authors">
|
||||
<span class="author-block"><sup>1</sup>University of California, Los Angeles,</span>
|
||||
<span class="author-block"><sup>2</sup>Massachusetts Institute of Technology</span>
|
||||
</div>
|
||||
<div class="column has-text-centered">
|
||||
<div class="publication-links">
|
||||
<span class="link-block"><a href="https://arxiv.org/abs/2412.20138" class="external-link button is-normal is-rounded is-dark"><span class="icon"><i class="fas fa-file-pdf"></i></span><span>Paper</span></a></span>
|
||||
<span class="link-block"><a href="https://github.com/TradingAgents-AI" class="external-link button is-normal is-rounded is-dark"><span class="icon"><i class="fab fa-github"></i></span><span>Code</span></a></span>
|
||||
<span class="link-block"><a href="#citation-ref" class="external-link button is-normal is-rounded is-dark"><span class="icon"><i class="fas fa-quote-right"></i></span><span>Citation</span></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered has-text-centered">
|
||||
<div class="column is-four-fifths">
|
||||
<h2 class="title is-3">Abstract</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p>We introduce <strong>TradingAgents</strong>, a novel stock trading framework inspired by trading firms, utilizing multiple LLM-powered agents with specialized roles such as fundamental, sentiment, and technical analysts, as well as traders with diverse risk profiles. The system features Bull and Bear researchers evaluating market conditions, a risk management team overseeing exposure, and traders integrating insights from debates and historical data to make informed decisions. This collaborative, dynamic environment enhances trading performance, as demonstrated by our comprehensive experiments showing significant improvements in cumulative returns, Sharpe ratio, and maximum drawdown compared to baseline models. Our results highlight the effectiveness of multi-agent LLM frameworks in financial trading.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-full-width">
|
||||
<h2 class="title is-3">TradingAgents: Overview</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p><strong>TradingAgents</strong> leverages a multi-agent framework to simulate a professional trading firm with distinct roles: fundamental, sentiment, and technical analysts; researchers; traders; and risk managers. These agents collaborate through structured communication and debates, enhancing decision-making and optimizing trading strategies.</p>
|
||||
<figure class="image">
|
||||
<img src="./static/images/schema.png" alt="TradingAgents Overall Framework Organization">
|
||||
<figcaption class="has-text-centered"><strong>Figure 1:</strong> TradingAgents Overall Framework Organization.<br><strong>I. Analysts Team</strong>: Four analysts concurrently gather relevant market information.<br><strong>II. Research Team</strong>: The team discusses and evaluates the collected data.<br><strong>III. Trader</strong>: Based on the researchers' analysis, the trader makes the trading decision.<br><strong>IV. Risk Management Team</strong>: Risk guardians assess the decision against current market conditions to mitigate risks.<br><strong>V. Fund Manager</strong>: The fund manager approves and executes the trade.</em></figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-full-width">
|
||||
<h2 class="title is-3">TradingAgents: Role Specialization</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p>Assigning specific roles to LLM agents allows complex trading objectives to be broken down into manageable tasks. Inspired by trading firms, <strong>TradingAgents</strong> features seven distinct roles: Fundamentals Analyst, Sentiment Analyst, News Analyst, Technical Analyst, Researcher, Trader, and Risk Manager. Each agent is equipped with specialized tools and constraints tailored to their function, ensuring comprehensive market analysis and informed decision-making.</p>
|
||||
|
||||
<h3 class="title is-4">Analyst Team</h3>
|
||||
<div class="content has-text-justified">
|
||||
<p>The Analyst Team gathers and analyzes market data across various domains:</p>
|
||||
<ul>
|
||||
<li><strong>Fundamental Analysts:</strong> Assess company fundamentals to identify undervalued or overvalued stocks.</li>
|
||||
<li><strong>Sentiment Analysts:</strong> Analyze social media and public sentiment to gauge market mood.</li>
|
||||
<li><strong>News Analysts:</strong> Evaluate news and macroeconomic indicators to predict market movements.</li>
|
||||
<li><strong>Technical Analysts:</strong> Use technical indicators to forecast price trends and trading opportunities.</li>
|
||||
</ul>
|
||||
<p>Combined, their insights provide a holistic market view, feeding into the Researcher Team for further evaluation.</p>
|
||||
|
||||
<figure class="image">
|
||||
<img src="./static/images/Analyst.png" alt="TradingAgents Analyst Team" style="width: 65%;">
|
||||
<figcaption class="has-text-centered"><strong>Figure 2:</strong> TradingAgents Analyst Team</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<h3 class="title is-4">Researcher Team</h3>
|
||||
<div class="content has-text-justified">
|
||||
<p>The Researcher Team critically evaluates analyst data through a dialectical process involving bullish and bearish perspectives. This debate ensures balanced analysis, identifying both opportunities and risks to inform trading strategies.</p>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<figure class="image">
|
||||
<img src="./static/images/Researcher.png" alt="TradingAgents Researcher Team">
|
||||
<figcaption class="has-text-centered"><strong>Figure 3:</strong> TradingAgents Researcher Team</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="column">
|
||||
<figure class="image">
|
||||
<img src="./static/images/Trader.png" alt="TradingAgents Trader Decision-Making Process">
|
||||
<figcaption class="has-text-centered"><strong>Figure 4:</strong> TradingAgents Trader Decision-Making Process</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="column">
|
||||
<figure class="image">
|
||||
<img src="./static/images/RiskMGMT.png" alt="TradingAgents Risk Management Team Workflow">
|
||||
<figcaption class="has-text-centered"><strong>Figure 5:</strong> TradingAgents Risk Management Workflow</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li><strong>Bullish Researchers:</strong> Highlight positive market indicators and growth potential.</li>
|
||||
<li><strong>Bearish Researchers:</strong> Focus on risks and negative market signals.</li>
|
||||
</ul>
|
||||
|
||||
<p>This process ensures a balanced understanding of market conditions, aiding Trader Agents in making informed decisions.</p>
|
||||
</div>
|
||||
|
||||
<h3 class="title is-4">Trader Agents</h3>
|
||||
<div class="content has-text-justified">
|
||||
<p>Trader Agents execute decisions based on comprehensive analyses. They evaluate insights from analysts and researchers to determine optimal trading actions, balancing returns and risks in a dynamic market environment.</p>
|
||||
|
||||
<ul>
|
||||
<li>Assessing analyst and researcher recommendations.</li>
|
||||
<li>Determining trade timing and size.</li>
|
||||
<li>Executing buy/sell orders.</li>
|
||||
<li>Adjusting portfolios in response to market changes.</li>
|
||||
</ul>
|
||||
|
||||
<p>Precision and strategic thinking are essential for their role in maximizing performance.</p>
|
||||
</div>
|
||||
|
||||
<h3 class="title is-4">Risk Management Team</h3>
|
||||
<div class="content has-text-justified">
|
||||
<p>The Risk Management Team oversees the firm's exposure to market risks, ensuring trading activities stay within predefined limits.</p>
|
||||
|
||||
<ul>
|
||||
<li>Assessing market volatility and liquidity.</li>
|
||||
<li>Implementing risk mitigation strategies.</li>
|
||||
<li>Advising Trader Agents on risk exposures.</li>
|
||||
<li>Aligning portfolio with risk tolerance.</li>
|
||||
</ul>
|
||||
|
||||
<p>They ensure financial stability and safeguard assets through effective risk control.</p>
|
||||
|
||||
<p>All agents utilize the ReAct prompting framework, facilitating a collaborative and dynamic decision-making process reflective of real-world trading systems.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-full-width">
|
||||
<h2 class="title is-3">TradingAgents: Agent Workflow</h2>
|
||||
<div class="content has-text-justified">
|
||||
<h3 class="title is-4">Communication Protocol</h3>
|
||||
<p>To enhance communication efficiency, <strong>TradingAgents</strong> employs a structured protocol that combines clear, structured outputs with natural language dialogue. This approach minimizes information loss and maintains context over long interactions, ensuring focused and effective communication among agents.</p>
|
||||
|
||||
<h3 class="title is-4">Types of Agent Interactions</h3>
|
||||
<p>Unlike previous frameworks that rely heavily on unstructured dialogue, our agents communicate through structured reports and diagrams, preserving essential information and enabling direct queries from the global state.</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Analyst Team:</strong> Compiles research into concise analysis reports.</li>
|
||||
<li><strong>Traders:</strong> Review analyst reports and produce decision signals with detailed rationales.</li>
|
||||
</ul>
|
||||
|
||||
<p>Natural language dialogue is reserved for specific interactions, such as debates within the Researcher and Risk Management teams, fostering deeper reasoning and balanced decision-making.</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Researcher Team:</strong> Engages in debates to form balanced perspectives.</li>
|
||||
<li><strong>Risk Management Team:</strong> Deliberates on trading plans from multiple risk perspectives.</li>
|
||||
<li><strong>Fund Manager:</strong> Reviews and approves risk-adjusted trading decisions.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="title is-4">Backbone LLMs</h3>
|
||||
<p>We select LLMs based on task requirements, using quick-thinking models for data retrieval and deep-thinking models for in-depth analysis and decision-making. This strategic alignment ensures efficiency and robust reasoning, allowing <strong>TradingAgents</strong> to operate without the need for GPUs and enabling easy integration of alternative models in the future.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-full-width">
|
||||
<h2 class="title is-3">Experiments</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p>We evaluated <strong>TradingAgents</strong> using a comprehensive experimental setup to assess its performance against various baselines.</p>
|
||||
|
||||
<h3 class="title is-4">Back Trading</h3>
|
||||
<p>Our simulation utilized a multi-asset, multi-modal financial dataset including historical stock prices, news articles, social media sentiments, insider transactions, financial reports, and technical indicators from January to March 2024.</p>
|
||||
|
||||
<h3 class="title is-4">Simulation Setup</h3>
|
||||
<p>The trading environment spanned from June to November 2024. Agents operated on a daily basis, making decisions based on available data without future information, ensuring unbiased results.</p>
|
||||
|
||||
<h3 class="title is-4">Baseline Models</h3>
|
||||
<p>We compared <strong>TradingAgents</strong> against the following strategies:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Buy and Hold:</strong> Investing equally across selected stocks throughout the period.</li>
|
||||
<li><strong>MACD:</strong> Momentum strategy based on MACD crossovers.</li>
|
||||
<li><strong>KDJ & RSI:</strong> Combined momentum indicators for trading signals.</li>
|
||||
<li><strong>ZMR:</strong> Mean reversion strategy based on price deviations.</li>
|
||||
<li><strong>SMA:</strong> Trend-following strategy using moving average crossovers.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="title is-4">Evaluation Metrics</h3>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<figure class="image">
|
||||
<img src="./static/images/CumulativeReturns_AAPL.png" alt="Cumulative Returns on AAPL">
|
||||
<figcaption class="has-text-centered"><strong>(a)</strong> Cumulative Returns on AAPL</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<figure class="image">
|
||||
<img src="./static/images/TradingAgents_Transactions_AAPL.png" alt="TradingAgents Transactions for AAPL">
|
||||
<figcaption class="has-text-centered">
|
||||
<strong>(b)</strong> TradingAgents Transactions for AAPL.<br>
|
||||
Green / Red Arrows for Long / Short Positions.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table is-striped is-fullwidth is-centered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Categories</th>
|
||||
<th>Models</th>
|
||||
<th colspan="4">AAPL</th>
|
||||
<th></th>
|
||||
<th colspan="4">GOOGL</th>
|
||||
<th></th>
|
||||
<th colspan="4">AMZN</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>CR%↑</th>
|
||||
<th>ARR%↑</th>
|
||||
<th>SR↑</th>
|
||||
<th>MDD%↓</th>
|
||||
<th></th>
|
||||
<th>CR%↑</th>
|
||||
<th>ARR%↑</th>
|
||||
<th>SR↑</th>
|
||||
<th>MDD%↓</th>
|
||||
<th></th>
|
||||
<th>CR%↑</th>
|
||||
<th>ARR%↑</th>
|
||||
<th>SR↑</th>
|
||||
<th>MDD%↓</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Market</td>
|
||||
<td>B&H</td>
|
||||
<td>-5.23</td><td>-5.09</td><td>-1.29</td><td>11.90</td>
|
||||
<td></td>
|
||||
<td>7.78</td><td>8.09</td><td>1.35</td><td>13.04</td>
|
||||
<td></td>
|
||||
<td>17.1</td><td>17.6</td><td>3.53</td><td>3.80</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">Rule-based</td>
|
||||
<td>MACD</td>
|
||||
<td>-1.49</td><td>-1.48</td><td>-0.81</td><td>4.53</td>
|
||||
<td></td>
|
||||
<td>6.20</td><td>6.26</td><td>2.31</td><td>1.22</td>
|
||||
<td></td>
|
||||
<td>-</td><td>-</td><td>-</td><td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>KDJ&RSI</td>
|
||||
<td>2.05</td><td>2.07</td><td>1.64</td><td>1.09</td>
|
||||
<td></td>
|
||||
<td>0.4</td><td>0.4</td><td>0.02</td><td>1.58</td>
|
||||
<td></td>
|
||||
<td>-0.77</td><td>-0.76</td><td>-2.25</td><td>1.08</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ZMR</td>
|
||||
<td>0.57</td><td>0.57</td><td>0.17</td><td>0.86</td>
|
||||
<td></td>
|
||||
<td>-0.58</td><td>0.58</td><td>2.12</td><td>2.34</td>
|
||||
<td></td>
|
||||
<td>-0.77</td><td>-0.77</td><td>-2.45</td><td>0.82</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SMA</td>
|
||||
<td>-3.2</td><td>-2.97</td><td>-1.72</td><td>3.67</td>
|
||||
<td></td>
|
||||
<td>6.23</td><td>6.43</td><td>2.12</td><td>2.34</td>
|
||||
<td></td>
|
||||
<td>11.01</td><td>11.6</td><td>2.22</td><td>3.97</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="1">Ours</td>
|
||||
<td><strong>TradingAgents</strong></td>
|
||||
<td><strong style="color:green;">26.62</strong></td><td><strong style="color:green;">30.5</strong></td><td><strong style="color:green;">8.21</strong></td><td>0.91</td>
|
||||
<td></td>
|
||||
<td><strong style="color:green;">24.36</strong></td><td><strong style="color:green;">27.58</strong></td><td><strong style="color:green;">6.39</strong></td><td>1.69</td>
|
||||
<td></td>
|
||||
<td><strong style="color:green;">23.21</strong></td><td><strong style="color:green;">24.90</strong></td><td><strong style="color:green;">5.60</strong></td><td>2.11</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">Improvement(%)</td>
|
||||
<td>24.57</td><td>28.43</td><td>6.57</td><td>-</td>
|
||||
<td></td>
|
||||
<td>16.58</td><td>19.49</td><td>4.26</td><td>-</td>
|
||||
<td></td>
|
||||
<td>6.10</td><td>7.30</td><td>2.07</td><td>-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="has-text-centered"><strong>Table 1:</strong> TradingAgents: Performance Metrics Comparison across AAPL, GOOGL, and AMZN.</p>
|
||||
|
||||
<h3 class="title is-4">Sharpe Ratio</h3>
|
||||
<p><strong>TradingAgents</strong> achieves superior risk-adjusted returns, consistently outperforming all baselines across AAPL, GOOGL, and AMZN. The enhanced Sharpe Ratios demonstrate the framework's effectiveness in balancing returns with risk, highlighting its robustness in diverse market conditions.</p>
|
||||
|
||||
<h3 class="title is-4">Maximum Drawdown</h3>
|
||||
<p>While rule-based strategies excel in controlling risk, <strong>TradingAgents</strong> maintains a low maximum drawdown without sacrificing high returns. This balance underscores the framework's ability to maximize profits while effectively managing risk.</p>
|
||||
|
||||
<h3 class="title is-4">Explainability</h3>
|
||||
<p>Unlike traditional deep learning models, <strong>TradingAgents</strong> offers transparent decision-making through natural language explanations. Each agent's actions are accompanied by detailed reasoning and tool usage, making the system's operations easily interpretable and debuggable, which is crucial for real-world financial applications.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-full-width">
|
||||
<h2 class="title is-3">Conclusion</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p>We presented <strong>TradingAgents</strong>, a multi-agent LLM-driven stock trading framework that emulates a realistic trading firm with specialized agents collaborating through debates and structured communication. Our framework leverages diverse data sources and multi-agent interactions to enhance trading decisions, achieving superior performance in cumulative returns, Sharpe ratio, and risk management compared to traditional strategies. Future work includes live deployment, expanding agent roles, and integrating real-time data processing to further improve performance.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="citation-ref" class="section">
|
||||
<div class="container content">
|
||||
<h2 class="title">BibTeX</h2>
|
||||
<pre><code>@inproceedings{xiao2025tradingagentsmultiagentsllmfinancial,
|
||||
title = {TradingAgents: Multi-Agents LLM Financial Trading Framework},
|
||||
author = {Yijia Xiao and Edward Sun and Di Luo and Wei Wang},
|
||||
booktitle = {Multi-Agent AI in the Real World @ AAAI 2025},
|
||||
year = {2025},
|
||||
eprint = {2412.20138},
|
||||
archivePrefix= {arXiv},
|
||||
primaryClass = {q-fin.TR},
|
||||
url = {https://arxiv.org/abs/2412.20138},
|
||||
note = {Workshop paper},
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content has-text-centered">
|
||||
<a class="icon-link" href="https://arxiv.org/abs/2412.20138"><i class="fas fa-file-pdf"></i></a>
|
||||
<a class="icon-link" href="https://github.com/TradingAgents-AI"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-8">
|
||||
<div class="content">
|
||||
<p>This website is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
18
main.py
18
main.py
|
|
@ -1,24 +1,12 @@
|
|||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Create a custom config
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["deep_think_llm"] = "gpt-5.4-mini" # Use a different model
|
||||
config["quick_think_llm"] = "gpt-5.4-mini" # Use a different model
|
||||
config["deep_think_llm"] = "gpt-4.1-nano" # Use a different model
|
||||
config["quick_think_llm"] = "gpt-4.1-nano" # Use a different model
|
||||
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
|
||||
}
|
||||
config["online_tools"] = True # Increase debate rounds
|
||||
|
||||
# Initialize with custom config
|
||||
ta = TradingAgentsGraph(debug=True, config=config)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tradingagents"
|
||||
version = "0.2.3"
|
||||
description = "TradingAgents: Multi-Agents LLM Financial Trading Framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-core>=0.3.81",
|
||||
"backtrader>=1.9.78.123",
|
||||
"langchain-anthropic>=0.3.15",
|
||||
"langchain-experimental>=0.3.4",
|
||||
"langchain-google-genai>=4.0.0",
|
||||
"langchain-openai>=0.3.23",
|
||||
"langgraph>=0.4.8",
|
||||
"pandas>=2.3.0",
|
||||
"parsel>=1.10.0",
|
||||
"pytz>=2025.2",
|
||||
"questionary>=2.1.0",
|
||||
"rank-bm25>=0.2.2",
|
||||
"redis>=6.2.0",
|
||||
"requests>=2.32.4",
|
||||
"rich>=14.0.0",
|
||||
"typer>=0.21.0",
|
||||
"setuptools>=80.9.0",
|
||||
"stockstats>=0.6.5",
|
||||
"tqdm>=4.67.1",
|
||||
"typing-extensions>=4.14.0",
|
||||
"yfinance>=0.2.63",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
tradingagents = "cli.main:app"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["tradingagents*", "cli*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
cli = ["static/*"]
|
||||
|
|
@ -1 +1,24 @@
|
|||
.
|
||||
typing-extensions
|
||||
langchain-openai
|
||||
langchain-experimental
|
||||
pandas
|
||||
yfinance
|
||||
praw
|
||||
feedparser
|
||||
stockstats
|
||||
eodhd
|
||||
langgraph
|
||||
chromadb
|
||||
setuptools
|
||||
backtrader
|
||||
akshare
|
||||
tushare
|
||||
finnhub-python
|
||||
parsel
|
||||
requests
|
||||
tqdm
|
||||
pytz
|
||||
redis
|
||||
chainlit
|
||||
rich
|
||||
questionary
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
Setup script for the TradingAgents package.
|
||||
"""
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="tradingagents",
|
||||
version="0.1.0",
|
||||
description="Multi-Agents LLM Financial Trading Framework",
|
||||
author="TradingAgents Team",
|
||||
author_email="yijia.xiao@cs.ucla.edu",
|
||||
url="https://github.com/TauricResearch",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"langchain>=0.1.0",
|
||||
"langchain-openai>=0.0.2",
|
||||
"langchain-experimental>=0.0.40",
|
||||
"langgraph>=0.0.20",
|
||||
"numpy>=1.24.0",
|
||||
"pandas>=2.0.0",
|
||||
"praw>=7.7.0",
|
||||
"stockstats>=0.5.4",
|
||||
"yfinance>=0.2.31",
|
||||
"typer>=0.9.0",
|
||||
"rich>=13.0.0",
|
||||
"questionary>=2.0.1",
|
||||
],
|
||||
python_requires=">=3.10",
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"tradingagents=cli.main:app",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Financial and Trading Industry",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Office/Business :: Financial :: Investment",
|
||||
],
|
||||
)
|
||||
11
test.py
11
test.py
|
|
@ -1,11 +0,0 @@
|
|||
import time
|
||||
from tradingagents.dataflows.y_finance import get_YFin_data_online, get_stock_stats_indicators_window, get_balance_sheet as get_yfinance_balance_sheet, get_cashflow as get_yfinance_cashflow, get_income_statement as get_yfinance_income_statement, get_insider_transactions as get_yfinance_insider_transactions
|
||||
|
||||
print("Testing optimized implementation with 30-day lookback:")
|
||||
start_time = time.time()
|
||||
result = get_stock_stats_indicators_window("AAPL", "macd", "2024-11-01", 30)
|
||||
end_time = time.time()
|
||||
|
||||
print(f"Execution time: {end_time - start_time:.2f} seconds")
|
||||
print(f"Result length: {len(result)} characters")
|
||||
print(result)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from tradingagents.llm_clients.google_client import GoogleClient
|
||||
|
||||
|
||||
class TestGoogleApiKeyStandardization(unittest.TestCase):
|
||||
"""Verify GoogleClient accepts unified api_key parameter."""
|
||||
|
||||
@patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI")
|
||||
def test_api_key_handling(self, mock_chat):
|
||||
test_cases = [
|
||||
("unified api_key is mapped", {"api_key": "test-key-123"}, "test-key-123"),
|
||||
("legacy google_api_key still works", {"google_api_key": "legacy-key-456"}, "legacy-key-456"),
|
||||
("unified api_key takes precedence", {"api_key": "unified", "google_api_key": "legacy"}, "unified"),
|
||||
]
|
||||
|
||||
for msg, kwargs, expected_key in test_cases:
|
||||
with self.subTest(msg=msg):
|
||||
mock_chat.reset_mock()
|
||||
client = GoogleClient("gemini-2.5-flash", **kwargs)
|
||||
client.get_llm()
|
||||
call_kwargs = mock_chat.call_args[1]
|
||||
self.assertEqual(call_kwargs.get("google_api_key"), expected_key)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import unittest
|
||||
import warnings
|
||||
|
||||
from tradingagents.llm_clients.base_client import BaseLLMClient
|
||||
from tradingagents.llm_clients.model_catalog import get_known_models
|
||||
from tradingagents.llm_clients.validators import validate_model
|
||||
|
||||
|
||||
class DummyLLMClient(BaseLLMClient):
|
||||
def __init__(self, provider: str, model: str):
|
||||
self.provider = provider
|
||||
super().__init__(model)
|
||||
|
||||
def get_llm(self):
|
||||
self.warn_if_unknown_model()
|
||||
return object()
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
return validate_model(self.provider, self.model)
|
||||
|
||||
|
||||
class ModelValidationTests(unittest.TestCase):
|
||||
def test_cli_catalog_models_are_all_validator_approved(self):
|
||||
for provider, models in get_known_models().items():
|
||||
if provider in ("ollama", "openrouter"):
|
||||
continue
|
||||
|
||||
for model in models:
|
||||
with self.subTest(provider=provider, model=model):
|
||||
self.assertTrue(validate_model(provider, model))
|
||||
|
||||
def test_unknown_model_emits_warning_for_strict_provider(self):
|
||||
client = DummyLLMClient("openai", "not-a-real-openai-model")
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
client.get_llm()
|
||||
|
||||
self.assertEqual(len(caught), 1)
|
||||
self.assertIn("not-a-real-openai-model", str(caught[0].message))
|
||||
self.assertIn("openai", str(caught[0].message))
|
||||
|
||||
def test_openrouter_and_ollama_accept_custom_models_without_warning(self):
|
||||
for provider in ("openrouter", "ollama"):
|
||||
client = DummyLLMClient(provider, "custom-model-name")
|
||||
|
||||
with self.subTest(provider=provider):
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
client.get_llm()
|
||||
|
||||
self.assertEqual(caught, [])
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import unittest
|
||||
|
||||
from cli.utils import normalize_ticker_symbol
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
|
||||
|
||||
class TickerSymbolHandlingTests(unittest.TestCase):
|
||||
def test_normalize_ticker_symbol_preserves_exchange_suffix(self):
|
||||
self.assertEqual(normalize_ticker_symbol(" cnc.to "), "CNC.TO")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import os
|
||||
os.environ.setdefault("PYTHONUTF8", "1")
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from .utils.agent_utils import create_msg_delete
|
||||
from .utils.agent_utils import Toolkit, create_msg_delete
|
||||
from .utils.agent_states import AgentState, InvestDebateState, RiskDebateState
|
||||
from .utils.memory import FinancialSituationMemory
|
||||
|
||||
|
|
@ -10,17 +10,18 @@ from .analysts.social_media_analyst import create_social_media_analyst
|
|||
from .researchers.bear_researcher import create_bear_researcher
|
||||
from .researchers.bull_researcher import create_bull_researcher
|
||||
|
||||
from .risk_mgmt.aggressive_debator import create_aggressive_debator
|
||||
from .risk_mgmt.conservative_debator import create_conservative_debator
|
||||
from .risk_mgmt.aggresive_debator import create_risky_debator
|
||||
from .risk_mgmt.conservative_debator import create_safe_debator
|
||||
from .risk_mgmt.neutral_debator import create_neutral_debator
|
||||
|
||||
from .managers.research_manager import create_research_manager
|
||||
from .managers.portfolio_manager import create_portfolio_manager
|
||||
from .managers.risk_manager import create_risk_manager
|
||||
|
||||
from .trader.trader import create_trader
|
||||
|
||||
__all__ = [
|
||||
"FinancialSituationMemory",
|
||||
"Toolkit",
|
||||
"AgentState",
|
||||
"create_msg_delete",
|
||||
"InvestDebateState",
|
||||
|
|
@ -32,9 +33,9 @@ __all__ = [
|
|||
"create_market_analyst",
|
||||
"create_neutral_debator",
|
||||
"create_news_analyst",
|
||||
"create_aggressive_debator",
|
||||
"create_portfolio_manager",
|
||||
"create_conservative_debator",
|
||||
"create_risky_debator",
|
||||
"create_risk_manager",
|
||||
"create_safe_debator",
|
||||
"create_social_media_analyst",
|
||||
"create_trader",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,33 +1,28 @@
|
|||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_fundamentals,
|
||||
get_income_statement,
|
||||
get_insider_transactions,
|
||||
get_language_instruction,
|
||||
)
|
||||
from tradingagents.dataflows.config import get_config
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_fundamentals_analyst(llm):
|
||||
def create_fundamentals_analyst(llm, toolkit):
|
||||
def fundamentals_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state["company_of_interest"]
|
||||
|
||||
tools = [
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_income_statement,
|
||||
]
|
||||
if toolkit.config["online_tools"]:
|
||||
tools = [toolkit.get_fundamentals_openai]
|
||||
else:
|
||||
tools = [
|
||||
toolkit.get_finnhub_company_insider_sentiment,
|
||||
toolkit.get_finnhub_company_insider_transactions,
|
||||
toolkit.get_simfin_balance_sheet,
|
||||
toolkit.get_simfin_cashflow,
|
||||
toolkit.get_simfin_income_stmt,
|
||||
]
|
||||
|
||||
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."
|
||||
+ " 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."
|
||||
+ get_language_instruction(),
|
||||
"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, company financial history, insider sentiment and insider transactions to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
|
||||
+ " Make sure to append a Makrdown table at the end of the report to organize key points in the report, organized and easy to read.",
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -41,7 +36,7 @@ def create_fundamentals_analyst(llm):
|
|||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. The company we want to look at is {ticker}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -50,20 +45,15 @@ def create_fundamentals_analyst(llm):
|
|||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = ""
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
"messages": [result],
|
||||
"fundamentals_report": report,
|
||||
"fundamentals_report": result.content,
|
||||
}
|
||||
|
||||
return fundamentals_analyst_node
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
get_indicators,
|
||||
get_language_instruction,
|
||||
get_stock_data,
|
||||
)
|
||||
from tradingagents.dataflows.config import get_config
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_market_analyst(llm):
|
||||
def create_market_analyst(llm, toolkit):
|
||||
|
||||
def market_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state["company_of_interest"]
|
||||
|
||||
tools = [
|
||||
get_stock_data,
|
||||
get_indicators,
|
||||
]
|
||||
if toolkit.config["online_tools"]:
|
||||
tools = [
|
||||
toolkit.get_YFin_data_online,
|
||||
toolkit.get_stockstats_indicators_report_online,
|
||||
]
|
||||
else:
|
||||
tools = [
|
||||
toolkit.get_YFin_data,
|
||||
toolkit.get_stockstats_indicators_report,
|
||||
]
|
||||
|
||||
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:
|
||||
|
|
@ -44,9 +46,8 @@ 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_YFin_data first to retrieve the CSV that is needed to generate indicators. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make 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()
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -60,7 +61,7 @@ Volume-Based Indicators:
|
|||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. The company we want to look at is {ticker}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -69,20 +70,15 @@ Volume-Based Indicators:
|
|||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = ""
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
"messages": [result],
|
||||
"market_report": report,
|
||||
"market_report": result.content,
|
||||
}
|
||||
|
||||
return market_analyst_node
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
get_global_news,
|
||||
get_language_instruction,
|
||||
get_news,
|
||||
)
|
||||
from tradingagents.dataflows.config import get_config
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_news_analyst(llm):
|
||||
def create_news_analyst(llm, toolkit):
|
||||
def news_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
get_global_news,
|
||||
]
|
||||
if toolkit.config["online_tools"]:
|
||||
tools = [toolkit.get_global_news_openai, toolkit.get_google_news]
|
||||
else:
|
||||
tools = [
|
||||
toolkit.get_finnhub_news,
|
||||
toolkit.get_reddit_news,
|
||||
toolkit.get_google_news,
|
||||
]
|
||||
|
||||
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."
|
||||
+ """ 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()
|
||||
"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. Look at news from EODHD, and finnhub to be comprehensive. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
|
||||
+ """ Make sure to append a Makrdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -35,7 +33,7 @@ def create_news_analyst(llm):
|
|||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. We are looking at the company {ticker}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -44,19 +42,14 @@ def create_news_analyst(llm):
|
|||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = ""
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
"messages": [result],
|
||||
"news_report": report,
|
||||
"news_report": result.content,
|
||||
}
|
||||
|
||||
return news_analyst_node
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction, get_news
|
||||
from tradingagents.dataflows.config import get_config
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_social_media_analyst(llm):
|
||||
def create_social_media_analyst(llm, toolkit):
|
||||
def social_media_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
company_name = state["company_of_interest"]
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
]
|
||||
if toolkit.config["online_tools"]:
|
||||
tools = [toolkit.get_stock_news_openai]
|
||||
else:
|
||||
tools = [
|
||||
toolkit.get_reddit_stock_info,
|
||||
]
|
||||
|
||||
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."
|
||||
+ """ 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()
|
||||
"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. Try to look at all sources possible from social media to sentiment to news. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
|
||||
+ """ Make sure to append a Makrdown table at the end of the report to organize key points in the report, organized and easy to read.""",
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -29,7 +32,7 @@ def create_social_media_analyst(llm):
|
|||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. The current company we want to analyze is {ticker}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -38,20 +41,15 @@ def create_social_media_analyst(llm):
|
|||
prompt = prompt.partial(system_message=system_message)
|
||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(ticker=ticker)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = ""
|
||||
|
||||
if len(result.tool_calls) == 0:
|
||||
report = result.content
|
||||
|
||||
return {
|
||||
"messages": [result],
|
||||
"sentiment_report": report,
|
||||
"sentiment_report": result.content,
|
||||
}
|
||||
|
||||
return social_media_analyst_node
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
|
||||
|
||||
|
||||
def create_portfolio_manager(llm, memory):
|
||||
def portfolio_manager_node(state) -> dict:
|
||||
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
|
||||
history = state["risk_debate_state"]["history"]
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
market_research_report = state["market_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
research_plan = state["investment_plan"]
|
||||
trader_plan = state["trader_investment_plan"]
|
||||
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
|
||||
prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision.
|
||||
|
||||
{instrument_context}
|
||||
|
||||
---
|
||||
|
||||
**Rating Scale** (use exactly one):
|
||||
- **Buy**: Strong conviction to enter or add to position
|
||||
- **Overweight**: Favorable outlook, gradually increase exposure
|
||||
- **Hold**: Maintain current position, no action needed
|
||||
- **Underweight**: Reduce exposure, take partial profits
|
||||
- **Sell**: Exit position or avoid entry
|
||||
|
||||
**Context:**
|
||||
- Research Manager's investment plan: **{research_plan}**
|
||||
- Trader's transaction proposal: **{trader_plan}**
|
||||
- Lessons from past decisions: **{past_memory_str}**
|
||||
|
||||
**Required Output Structure:**
|
||||
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
|
||||
2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon.
|
||||
3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and past reflections.
|
||||
|
||||
---
|
||||
|
||||
**Risk Analysts Debate History:**
|
||||
{history}
|
||||
|
||||
---
|
||||
|
||||
Be decisive and ground every conclusion in specific evidence from the analysts.{get_language_instruction()}"""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
new_risk_debate_state = {
|
||||
"judge_decision": response.content,
|
||||
"history": risk_debate_state["history"],
|
||||
"aggressive_history": risk_debate_state["aggressive_history"],
|
||||
"conservative_history": risk_debate_state["conservative_history"],
|
||||
"neutral_history": risk_debate_state["neutral_history"],
|
||||
"latest_speaker": "Judge",
|
||||
"current_aggressive_response": risk_debate_state["current_aggressive_response"],
|
||||
"current_conservative_response": risk_debate_state["current_conservative_response"],
|
||||
"current_neutral_response": risk_debate_state["current_neutral_response"],
|
||||
"count": risk_debate_state["count"],
|
||||
}
|
||||
|
||||
return {
|
||||
"risk_debate_state": new_risk_debate_state,
|
||||
"final_trade_decision": response.content,
|
||||
}
|
||||
|
||||
return portfolio_manager_node
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_research_manager(llm, memory):
|
||||
def research_manager_node(state) -> dict:
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
history = state["investment_debate_state"].get("history", "")
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
|
|
@ -34,8 +33,6 @@ Take into account your past mistakes on similar situations. Use these insights t
|
|||
Here are your past reflections on mistakes:
|
||||
\"{past_memory_str}\"
|
||||
|
||||
{instrument_context}
|
||||
|
||||
Here is the debate:
|
||||
Debate History:
|
||||
{history}"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_risk_manager(llm, memory):
|
||||
def risk_manager_node(state) -> dict:
|
||||
|
||||
company_name = state["company_of_interest"]
|
||||
|
||||
history = state["risk_debate_state"]["history"]
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
market_research_report = state["market_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["news_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
trader_plan = state["investment_plan"]
|
||||
|
||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
past_memory_str = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
|
||||
prompt = f"""As the Risk Management Judge and Debate Facilitator, your goal is to evaluate the debate between three risk analysts—Risky, Neutral, and Safe/Conservative—and determine the best course of action for the trader. Your decision must result in a clear recommendation: Buy, Sell, or Hold. Choose Hold only if strongly justified by specific arguments, not as a fallback when all sides seem valid. Strive for clarity and decisiveness.
|
||||
|
||||
Guidelines for Decision-Making:
|
||||
1. **Summarize Key Arguments**: Extract the strongest points from each analyst, focusing on relevance to the context.
|
||||
2. **Provide Rationale**: Support your recommendation with direct quotes and counterarguments from the debate.
|
||||
3. **Refine the Trader's Plan**: Start with the trader's original plan, **{trader_plan}**, and adjust it based on the analysts' insights.
|
||||
4. **Learn from Past Mistakes**: Use lessons from **{past_memory_str}** to address prior misjudgments and improve the decision you are making now to make sure you don't make a wrong BUY/SELL/HOLD call that loses money.
|
||||
|
||||
Deliverables:
|
||||
- A clear and actionable recommendation: Buy, Sell, or Hold.
|
||||
- Detailed reasoning anchored in the debate and past reflections.
|
||||
|
||||
---
|
||||
|
||||
**Analysts Debate History:**
|
||||
{history}
|
||||
|
||||
---
|
||||
|
||||
Focus on actionable insights and continuous improvement. Build on past lessons, critically evaluate all perspectives, and ensure each decision advances better outcomes."""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
new_risk_debate_state = {
|
||||
"judge_decision": response.content,
|
||||
"history": risk_debate_state["history"],
|
||||
"risky_history": risk_debate_state["risky_history"],
|
||||
"safe_history": risk_debate_state["safe_history"],
|
||||
"neutral_history": risk_debate_state["neutral_history"],
|
||||
"latest_speaker": "Judge",
|
||||
"current_risky_response": risk_debate_state["current_risky_response"],
|
||||
"current_safe_response": risk_debate_state["current_safe_response"],
|
||||
"current_neutral_response": risk_debate_state["current_neutral_response"],
|
||||
"count": risk_debate_state["count"],
|
||||
}
|
||||
|
||||
return {
|
||||
"risk_debate_state": new_risk_debate_state,
|
||||
"final_trade_decision": response.content,
|
||||
}
|
||||
|
||||
return risk_manager_node
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
from langchain_core.messages import AIMessage
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_bear_researcher(llm, memory):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from langchain_core.messages import AIMessage
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_bull_researcher(llm, memory):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_aggressive_debator(llm):
|
||||
def aggressive_node(state) -> dict:
|
||||
def create_risky_debator(llm):
|
||||
def risky_node(state) -> dict:
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state.get("history", "")
|
||||
aggressive_history = risk_debate_state.get("aggressive_history", "")
|
||||
risky_history = risk_debate_state.get("risky_history", "")
|
||||
|
||||
current_conservative_response = risk_debate_state.get("current_conservative_response", "")
|
||||
current_safe_response = risk_debate_state.get("current_safe_response", "")
|
||||
current_neutral_response = risk_debate_state.get("current_neutral_response", "")
|
||||
|
||||
market_research_report = state["market_report"]
|
||||
|
|
@ -16,7 +18,7 @@ def create_aggressive_debator(llm):
|
|||
|
||||
trader_decision = state["trader_investment_plan"]
|
||||
|
||||
prompt = f"""As the Aggressive Risk Analyst, your role is to actively champion high-reward, high-risk opportunities, emphasizing bold strategies and competitive advantages. When evaluating the trader's decision or plan, focus intently on the potential upside, growth potential, and innovative benefits—even when these come with elevated risk. Use the provided market data and sentiment analysis to strengthen your arguments and challenge the opposing views. Specifically, respond directly to each point made by the conservative and neutral analysts, countering with data-driven rebuttals and persuasive reasoning. Highlight where their caution might miss critical opportunities or where their assumptions may be overly conservative. Here is the trader's decision:
|
||||
prompt = f"""As the Risky Risk Analyst, your role is to actively champion high-reward, high-risk opportunities, emphasizing bold strategies and competitive advantages. When evaluating the trader's decision or plan, focus intently on the potential upside, growth potential, and innovative benefits—even when these come with elevated risk. Use the provided market data and sentiment analysis to strengthen your arguments and challenge the opposing views. Specifically, respond directly to each point made by the conservative and neutral analysts, countering with data-driven rebuttals and persuasive reasoning. Highlight where their caution might miss critical opportunities or where their assumptions may be overly conservative. Here is the trader's decision:
|
||||
|
||||
{trader_decision}
|
||||
|
||||
|
|
@ -26,22 +28,22 @@ Market Research Report: {market_research_report}
|
|||
Social Media Sentiment Report: {sentiment_report}
|
||||
Latest World Affairs Report: {news_report}
|
||||
Company Fundamentals Report: {fundamentals_report}
|
||||
Here is the current conversation history: {history} Here are the last arguments from the conservative analyst: {current_conservative_response} Here are the last arguments from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
|
||||
Here is the current conversation history: {history} Here are the last arguments from the conservative analyst: {current_safe_response} Here are the last arguments from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints, do not halluncinate and just present your point.
|
||||
|
||||
Engage actively by addressing any specific concerns raised, refuting the weaknesses in their logic, and asserting the benefits of risk-taking to outpace market norms. Maintain a focus on debating and persuading, not just presenting data. Challenge each counterpoint to underscore why a high-risk approach is optimal. Output conversationally as if you are speaking without any special formatting."""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
argument = f"Aggressive Analyst: {response.content}"
|
||||
argument = f"Risky Analyst: {response.content}"
|
||||
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"aggressive_history": aggressive_history + "\n" + argument,
|
||||
"conservative_history": risk_debate_state.get("conservative_history", ""),
|
||||
"risky_history": risky_history + "\n" + argument,
|
||||
"safe_history": risk_debate_state.get("safe_history", ""),
|
||||
"neutral_history": risk_debate_state.get("neutral_history", ""),
|
||||
"latest_speaker": "Aggressive",
|
||||
"current_aggressive_response": argument,
|
||||
"current_conservative_response": risk_debate_state.get("current_conservative_response", ""),
|
||||
"latest_speaker": "Risky",
|
||||
"current_risky_response": argument,
|
||||
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
|
||||
"current_neutral_response": risk_debate_state.get(
|
||||
"current_neutral_response", ""
|
||||
),
|
||||
|
|
@ -50,4 +52,4 @@ Engage actively by addressing any specific concerns raised, refuting the weaknes
|
|||
|
||||
return {"risk_debate_state": new_risk_debate_state}
|
||||
|
||||
return aggressive_node
|
||||
return risky_node
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
from langchain_core.messages import AIMessage
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_conservative_debator(llm):
|
||||
def conservative_node(state) -> dict:
|
||||
def create_safe_debator(llm):
|
||||
def safe_node(state) -> dict:
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
history = risk_debate_state.get("history", "")
|
||||
conservative_history = risk_debate_state.get("conservative_history", "")
|
||||
safe_history = risk_debate_state.get("safe_history", "")
|
||||
|
||||
current_aggressive_response = risk_debate_state.get("current_aggressive_response", "")
|
||||
current_risky_response = risk_debate_state.get("current_risky_response", "")
|
||||
current_neutral_response = risk_debate_state.get("current_neutral_response", "")
|
||||
|
||||
market_research_report = state["market_report"]
|
||||
|
|
@ -16,34 +19,34 @@ def create_conservative_debator(llm):
|
|||
|
||||
trader_decision = state["trader_investment_plan"]
|
||||
|
||||
prompt = f"""As the Conservative Risk Analyst, your primary objective is to protect assets, minimize volatility, and ensure steady, reliable growth. You prioritize stability, security, and risk mitigation, carefully assessing potential losses, economic downturns, and market volatility. When evaluating the trader's decision or plan, critically examine high-risk elements, pointing out where the decision may expose the firm to undue risk and where more cautious alternatives could secure long-term gains. Here is the trader's decision:
|
||||
prompt = f"""As the Safe/Conservative Risk Analyst, your primary objective is to protect assets, minimize volatility, and ensure steady, reliable growth. You prioritize stability, security, and risk mitigation, carefully assessing potential losses, economic downturns, and market volatility. When evaluating the trader's decision or plan, critically examine high-risk elements, pointing out where the decision may expose the firm to undue risk and where more cautious alternatives could secure long-term gains. Here is the trader's decision:
|
||||
|
||||
{trader_decision}
|
||||
|
||||
Your task is to actively counter the arguments of the Aggressive and Neutral Analysts, highlighting where their views may overlook potential threats or fail to prioritize sustainability. Respond directly to their points, drawing from the following data sources to build a convincing case for a low-risk approach adjustment to the trader's decision:
|
||||
Your task is to actively counter the arguments of the Risky and Neutral Analysts, highlighting where their views may overlook potential threats or fail to prioritize sustainability. Respond directly to their points, drawing from the following data sources to build a convincing case for a low-risk approach adjustment to the trader's decision:
|
||||
|
||||
Market Research Report: {market_research_report}
|
||||
Social Media Sentiment Report: {sentiment_report}
|
||||
Latest World Affairs Report: {news_report}
|
||||
Company Fundamentals Report: {fundamentals_report}
|
||||
Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
|
||||
Here is the current conversation history: {history} Here is the last response from the risky analyst: {current_risky_response} Here is the last response from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints, do not halluncinate and just present your point.
|
||||
|
||||
Engage by questioning their optimism and emphasizing the potential downsides they may have overlooked. Address each of their counterpoints to showcase why a conservative stance is ultimately the safest path for the firm's assets. Focus on debating and critiquing their arguments to demonstrate the strength of a low-risk strategy over their approaches. Output conversationally as if you are speaking without any special formatting."""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
argument = f"Conservative Analyst: {response.content}"
|
||||
argument = f"Safe Analyst: {response.content}"
|
||||
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"aggressive_history": risk_debate_state.get("aggressive_history", ""),
|
||||
"conservative_history": conservative_history + "\n" + argument,
|
||||
"risky_history": risk_debate_state.get("risky_history", ""),
|
||||
"safe_history": safe_history + "\n" + argument,
|
||||
"neutral_history": risk_debate_state.get("neutral_history", ""),
|
||||
"latest_speaker": "Conservative",
|
||||
"current_aggressive_response": risk_debate_state.get(
|
||||
"current_aggressive_response", ""
|
||||
"latest_speaker": "Safe",
|
||||
"current_risky_response": risk_debate_state.get(
|
||||
"current_risky_response", ""
|
||||
),
|
||||
"current_conservative_response": argument,
|
||||
"current_safe_response": argument,
|
||||
"current_neutral_response": risk_debate_state.get(
|
||||
"current_neutral_response", ""
|
||||
),
|
||||
|
|
@ -52,4 +55,4 @@ Engage by questioning their optimism and emphasizing the potential downsides the
|
|||
|
||||
return {"risk_debate_state": new_risk_debate_state}
|
||||
|
||||
return conservative_node
|
||||
return safe_node
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_neutral_debator(llm):
|
||||
|
|
@ -6,8 +8,8 @@ def create_neutral_debator(llm):
|
|||
history = risk_debate_state.get("history", "")
|
||||
neutral_history = risk_debate_state.get("neutral_history", "")
|
||||
|
||||
current_aggressive_response = risk_debate_state.get("current_aggressive_response", "")
|
||||
current_conservative_response = risk_debate_state.get("current_conservative_response", "")
|
||||
current_risky_response = risk_debate_state.get("current_risky_response", "")
|
||||
current_safe_response = risk_debate_state.get("current_safe_response", "")
|
||||
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
|
|
@ -20,15 +22,15 @@ def create_neutral_debator(llm):
|
|||
|
||||
{trader_decision}
|
||||
|
||||
Your task is to challenge both the Aggressive and Conservative Analysts, pointing out where each perspective may be overly optimistic or overly cautious. Use insights from the following data sources to support a moderate, sustainable strategy to adjust the trader's decision:
|
||||
Your task is to challenge both the Risky and Safe Analysts, pointing out where each perspective may be overly optimistic or overly cautious. Use insights from the following data sources to support a moderate, sustainable strategy to adjust the trader's decision:
|
||||
|
||||
Market Research Report: {market_research_report}
|
||||
Social Media Sentiment Report: {sentiment_report}
|
||||
Latest World Affairs Report: {news_report}
|
||||
Company Fundamentals Report: {fundamentals_report}
|
||||
Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the conservative analyst: {current_conservative_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
|
||||
Here is the current conversation history: {history} Here is the last response from the risky analyst: {current_risky_response} Here is the last response from the safe analyst: {current_safe_response}. If there are no responses from the other viewpoints, do not halluncinate and just present your point.
|
||||
|
||||
Engage actively by analyzing both sides critically, addressing weaknesses in the aggressive and conservative arguments to advocate for a more balanced approach. Challenge each of their points to illustrate why a moderate risk strategy might offer the best of both worlds, providing growth potential while safeguarding against extreme volatility. Focus on debating rather than simply presenting data, aiming to show that a balanced view can lead to the most reliable outcomes. Output conversationally as if you are speaking without any special formatting."""
|
||||
Engage actively by analyzing both sides critically, addressing weaknesses in the risky and conservative arguments to advocate for a more balanced approach. Challenge each of their points to illustrate why a moderate risk strategy might offer the best of both worlds, providing growth potential while safeguarding against extreme volatility. Focus on debating rather than simply presenting data, aiming to show that a balanced view can lead to the most reliable outcomes. Output conversationally as if you are speaking without any special formatting."""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
|
|
@ -36,14 +38,14 @@ Engage actively by analyzing both sides critically, addressing weaknesses in the
|
|||
|
||||
new_risk_debate_state = {
|
||||
"history": history + "\n" + argument,
|
||||
"aggressive_history": risk_debate_state.get("aggressive_history", ""),
|
||||
"conservative_history": risk_debate_state.get("conservative_history", ""),
|
||||
"risky_history": risk_debate_state.get("risky_history", ""),
|
||||
"safe_history": risk_debate_state.get("safe_history", ""),
|
||||
"neutral_history": neutral_history + "\n" + argument,
|
||||
"latest_speaker": "Neutral",
|
||||
"current_aggressive_response": risk_debate_state.get(
|
||||
"current_aggressive_response", ""
|
||||
"current_risky_response": risk_debate_state.get(
|
||||
"current_risky_response", ""
|
||||
),
|
||||
"current_conservative_response": risk_debate_state.get("current_conservative_response", ""),
|
||||
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
|
||||
"current_neutral_response": argument,
|
||||
"count": risk_debate_state["count"] + 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import functools
|
||||
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def create_trader(llm, memory):
|
||||
def trader_node(state, name):
|
||||
company_name = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(company_name)
|
||||
investment_plan = state["investment_plan"]
|
||||
market_research_report = state["market_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
|
|
@ -17,21 +16,18 @@ def create_trader(llm, memory):
|
|||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
||||
|
||||
past_memory_str = ""
|
||||
if past_memories:
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
else:
|
||||
past_memory_str = "No past memories found."
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
|
||||
context = {
|
||||
"role": "user",
|
||||
"content": f"Based on a comprehensive analysis by a team of analysts, here is an investment plan tailored for {company_name}. {instrument_context} This plan incorporates insights from current technical market trends, macroeconomic indicators, and social media sentiment. Use this plan as a foundation for evaluating your next trading decision.\n\nProposed Investment Plan: {investment_plan}\n\nLeverage these insights to make an informed and strategic decision.",
|
||||
"content": f"Based on a comprehensive analysis by a team of analysts, here is an investment plan tailored for {company_name}. This plan incorporates insights from current technical market trends, macroeconomic indicators, and social media sentiment. Use this plan as a foundation for evaluating your next trading decision.\n\nProposed Investment Plan: {investment_plan}\n\nLeverage these insights to make an informed and strategic decision.",
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation. Apply lessons from past decisions to strengthen your analysis. Here are reflections from similar situations you traded in and the lessons learned: {past_memory_str}""",
|
||||
"content": f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation. Do not forget to utilize lessons from past decisions to learn from your mistakes. Here is some reflections from similar situatiosn you traded in and the lessons learned: {past_memory_str}""",
|
||||
},
|
||||
context,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from typing import Annotated
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph import MessagesState
|
||||
from typing import Annotated, Sequence
|
||||
from datetime import date, timedelta, datetime
|
||||
from typing_extensions import TypedDict, Optional
|
||||
from langchain_openai import ChatOpenAI
|
||||
from tradingagents.agents import *
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.graph import END, StateGraph, START, MessagesState
|
||||
|
||||
|
||||
# Researcher team state
|
||||
|
|
@ -19,22 +23,22 @@ class InvestDebateState(TypedDict):
|
|||
|
||||
# Risk management team state
|
||||
class RiskDebateState(TypedDict):
|
||||
aggressive_history: Annotated[
|
||||
str, "Aggressive Agent's Conversation history"
|
||||
risky_history: Annotated[
|
||||
str, "Risky Agent's Conversation history"
|
||||
] # Conversation history
|
||||
conservative_history: Annotated[
|
||||
str, "Conservative Agent's Conversation history"
|
||||
safe_history: Annotated[
|
||||
str, "Safe Agent's Conversation history"
|
||||
] # Conversation history
|
||||
neutral_history: Annotated[
|
||||
str, "Neutral Agent's Conversation history"
|
||||
] # Conversation history
|
||||
history: Annotated[str, "Conversation history"] # Conversation history
|
||||
latest_speaker: Annotated[str, "Analyst that spoke last"]
|
||||
current_aggressive_response: Annotated[
|
||||
str, "Latest response by the aggressive analyst"
|
||||
current_risky_response: Annotated[
|
||||
str, "Latest response by the risky analyst"
|
||||
] # Last response
|
||||
current_conservative_response: Annotated[
|
||||
str, "Latest response by the conservative analyst"
|
||||
current_safe_response: Annotated[
|
||||
str, "Latest response by the safe analyst"
|
||||
] # Last response
|
||||
current_neutral_response: Annotated[
|
||||
str, "Latest response by the neutral analyst"
|
||||
|
|
|
|||
|
|
@ -1,61 +1,411 @@
|
|||
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage, AIMessage
|
||||
from typing import List
|
||||
from typing import Annotated
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.messages import RemoveMessage
|
||||
from langchain_core.tools import tool
|
||||
from datetime import date, timedelta, datetime
|
||||
import functools
|
||||
import pandas as pd
|
||||
import os
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from langchain_openai import ChatOpenAI
|
||||
import tradingagents.dataflows.interface as interface
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
# Import tools from separate utility files
|
||||
from tradingagents.agents.utils.core_stock_tools import (
|
||||
get_stock_data
|
||||
)
|
||||
from tradingagents.agents.utils.technical_indicators_tools import (
|
||||
get_indicators
|
||||
)
|
||||
from tradingagents.agents.utils.fundamental_data_tools import (
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_income_statement
|
||||
)
|
||||
from tradingagents.agents.utils.news_data_tools import (
|
||||
get_news,
|
||||
get_insider_transactions,
|
||||
get_global_news
|
||||
)
|
||||
|
||||
|
||||
def get_language_instruction() -> str:
|
||||
"""Return a prompt instruction for the configured output language.
|
||||
|
||||
Returns empty string when English (default), so no extra tokens are used.
|
||||
Only applied to user-facing agents (analysts, portfolio manager).
|
||||
Internal debate agents stay in English for reasoning quality.
|
||||
"""
|
||||
from tradingagents.dataflows.config import get_config
|
||||
lang = get_config().get("output_language", "English")
|
||||
if lang.strip().lower() == "english":
|
||||
return ""
|
||||
return f" Write your entire response in {lang}."
|
||||
|
||||
|
||||
def build_instrument_context(ticker: str) -> str:
|
||||
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
|
||||
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 create_msg_delete():
|
||||
def delete_messages(state):
|
||||
"""Clear messages and add placeholder for Anthropic compatibility"""
|
||||
"""To prevent message history from overflowing, regularly clear message history after a stage of the pipeline is done"""
|
||||
messages = state["messages"]
|
||||
|
||||
# Remove all messages
|
||||
removal_operations = [RemoveMessage(id=m.id) for m in messages]
|
||||
|
||||
# Add a minimal placeholder message
|
||||
placeholder = HumanMessage(content="Continue")
|
||||
|
||||
return {"messages": removal_operations + [placeholder]}
|
||||
return {"messages": [RemoveMessage(id=m.id) for m in messages]}
|
||||
|
||||
return delete_messages
|
||||
|
||||
|
||||
class Toolkit:
|
||||
_config = DEFAULT_CONFIG.copy()
|
||||
|
||||
@classmethod
|
||||
def update_config(cls, config):
|
||||
"""Update the class-level configuration."""
|
||||
cls._config.update(config)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Access the configuration."""
|
||||
return self._config
|
||||
|
||||
def __init__(self, config=None):
|
||||
if config:
|
||||
self.update_config(config)
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_reddit_news(
|
||||
curr_date: Annotated[str, "Date you want to get news for in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve global news from Reddit within a specified time frame.
|
||||
Args:
|
||||
curr_date (str): Date you want to get news for in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the latest global news from Reddit in the specified time frame.
|
||||
"""
|
||||
|
||||
global_news_result = interface.get_reddit_global_news(curr_date, 7, 5)
|
||||
|
||||
return global_news_result
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_finnhub_news(
|
||||
ticker: Annotated[
|
||||
str,
|
||||
"Search query of a company, e.g. 'AAPL, TSM, etc.",
|
||||
],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||
):
|
||||
"""
|
||||
Retrieve the latest news about a given stock from Finnhub within a date range
|
||||
Args:
|
||||
ticker (str): Ticker of a company. e.g. AAPL, TSM
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing news about the company within the date range from start_date to end_date
|
||||
"""
|
||||
|
||||
end_date_str = end_date
|
||||
|
||||
end_date = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
look_back_days = (end_date - start_date).days
|
||||
|
||||
finnhub_news_result = interface.get_finnhub_news(
|
||||
ticker, end_date_str, look_back_days
|
||||
)
|
||||
|
||||
return finnhub_news_result
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_reddit_stock_info(
|
||||
ticker: Annotated[
|
||||
str,
|
||||
"Ticker of a company. e.g. AAPL, TSM",
|
||||
],
|
||||
curr_date: Annotated[str, "Current date you want to get news for"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the latest news about a given stock from Reddit, given the current date.
|
||||
Args:
|
||||
ticker (str): Ticker of a company. e.g. AAPL, TSM
|
||||
curr_date (str): current date in yyyy-mm-dd format to get news for
|
||||
Returns:
|
||||
str: A formatted dataframe containing the latest news about the company on the given date
|
||||
"""
|
||||
|
||||
stock_news_results = interface.get_reddit_company_news(ticker, curr_date, 7, 5)
|
||||
|
||||
return stock_news_results
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_YFin_data(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the stock price data for a given ticker symbol from Yahoo Finance.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the stock price data for the specified ticker symbol in the specified date range.
|
||||
"""
|
||||
|
||||
result_data = interface.get_YFin_data(symbol, start_date, end_date)
|
||||
|
||||
return result_data
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_YFin_data_online(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the stock price data for a given ticker symbol from Yahoo Finance.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the stock price data for the specified ticker symbol in the specified date range.
|
||||
"""
|
||||
|
||||
result_data = interface.get_YFin_data_online(symbol, start_date, end_date)
|
||||
|
||||
return result_data
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_stockstats_indicators_report(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[
|
||||
str, "technical indicator to get the analysis and report of"
|
||||
],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
look_back_days: Annotated[int, "how many days to look back"] = 30,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve stock stats indicators for a given ticker symbol and indicator.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
indicator (str): Technical indicator to get the analysis and report of
|
||||
curr_date (str): The current trading date you are trading on, YYYY-mm-dd
|
||||
look_back_days (int): How many days to look back, default is 30
|
||||
Returns:
|
||||
str: A formatted dataframe containing the stock stats indicators for the specified ticker symbol and indicator.
|
||||
"""
|
||||
|
||||
result_stockstats = interface.get_stock_stats_indicators_window(
|
||||
symbol, indicator, curr_date, look_back_days, False
|
||||
)
|
||||
|
||||
return result_stockstats
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_stockstats_indicators_report_online(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[
|
||||
str, "technical indicator to get the analysis and report of"
|
||||
],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
look_back_days: Annotated[int, "how many days to look back"] = 30,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve stock stats indicators for a given ticker symbol and indicator.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
indicator (str): Technical indicator to get the analysis and report of
|
||||
curr_date (str): The current trading date you are trading on, YYYY-mm-dd
|
||||
look_back_days (int): How many days to look back, default is 30
|
||||
Returns:
|
||||
str: A formatted dataframe containing the stock stats indicators for the specified ticker symbol and indicator.
|
||||
"""
|
||||
|
||||
result_stockstats = interface.get_stock_stats_indicators_window(
|
||||
symbol, indicator, curr_date, look_back_days, True
|
||||
)
|
||||
|
||||
return result_stockstats
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_finnhub_company_insider_sentiment(
|
||||
ticker: Annotated[str, "ticker symbol for the company"],
|
||||
curr_date: Annotated[
|
||||
str,
|
||||
"current date of you are trading at, yyyy-mm-dd",
|
||||
],
|
||||
):
|
||||
"""
|
||||
Retrieve insider sentiment information about a company (retrieved from public SEC information) for the past 30 days
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the sentiment in the past 30 days starting at curr_date
|
||||
"""
|
||||
|
||||
data_sentiment = interface.get_finnhub_company_insider_sentiment(
|
||||
ticker, curr_date, 30
|
||||
)
|
||||
|
||||
return data_sentiment
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_finnhub_company_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
curr_date: Annotated[
|
||||
str,
|
||||
"current date you are trading at, yyyy-mm-dd",
|
||||
],
|
||||
):
|
||||
"""
|
||||
Retrieve insider transaction information about a company (retrieved from public SEC information) for the past 30 days
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the company's insider transactions/trading information in the past 30 days
|
||||
"""
|
||||
|
||||
data_trans = interface.get_finnhub_company_insider_transactions(
|
||||
ticker, curr_date, 30
|
||||
)
|
||||
|
||||
return data_trans
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_simfin_balance_sheet(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual/quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
"""
|
||||
Retrieve the most recent balance sheet of a company
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
freq (str): reporting frequency of the company's financial history: annual / quarterly
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the company's most recent balance sheet
|
||||
"""
|
||||
|
||||
data_balance_sheet = interface.get_simfin_balance_sheet(ticker, freq, curr_date)
|
||||
|
||||
return data_balance_sheet
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_simfin_cashflow(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual/quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
"""
|
||||
Retrieve the most recent cash flow statement of a company
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
freq (str): reporting frequency of the company's financial history: annual / quarterly
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the company's most recent cash flow statement
|
||||
"""
|
||||
|
||||
data_cashflow = interface.get_simfin_cashflow(ticker, freq, curr_date)
|
||||
|
||||
return data_cashflow
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_simfin_income_stmt(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual/quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
"""
|
||||
Retrieve the most recent income statement of a company
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
freq (str): reporting frequency of the company's financial history: annual / quarterly
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the company's most recent income statement
|
||||
"""
|
||||
|
||||
data_income_stmt = interface.get_simfin_income_statements(
|
||||
ticker, freq, curr_date
|
||||
)
|
||||
|
||||
return data_income_stmt
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_google_news(
|
||||
query: Annotated[str, "Query to search with"],
|
||||
curr_date: Annotated[str, "Curr date in yyyy-mm-dd format"],
|
||||
):
|
||||
"""
|
||||
Retrieve the latest news from Google News based on a query and date range.
|
||||
Args:
|
||||
query (str): Query to search with
|
||||
curr_date (str): Current date in yyyy-mm-dd format
|
||||
look_back_days (int): How many days to look back
|
||||
Returns:
|
||||
str: A formatted string containing the latest news from Google News based on the query and date range.
|
||||
"""
|
||||
|
||||
google_news_results = interface.get_google_news(query, curr_date, 7)
|
||||
|
||||
return google_news_results
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_stock_news_openai(
|
||||
ticker: Annotated[str, "the company's ticker"],
|
||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||
):
|
||||
"""
|
||||
Retrieve the latest news about a given stock by using OpenAI's news API.
|
||||
Args:
|
||||
ticker (str): Ticker of a company. e.g. AAPL, TSM
|
||||
curr_date (str): Current date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted string containing the latest news about the company on the given date.
|
||||
"""
|
||||
|
||||
openai_news_results = interface.get_stock_news_openai(ticker, curr_date)
|
||||
|
||||
return openai_news_results
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_global_news_openai(
|
||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||
):
|
||||
"""
|
||||
Retrieve the latest macroeconomics news on a given date using OpenAI's macroeconomics news API.
|
||||
Args:
|
||||
curr_date (str): Current date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted string containing the latest macroeconomic news on the given date.
|
||||
"""
|
||||
|
||||
openai_news_results = interface.get_global_news_openai(curr_date)
|
||||
|
||||
return openai_news_results
|
||||
|
||||
@staticmethod
|
||||
@tool
|
||||
def get_fundamentals_openai(
|
||||
ticker: Annotated[str, "the company's ticker"],
|
||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||
):
|
||||
"""
|
||||
Retrieve the latest fundamental information about a given stock on a given date by using OpenAI's news API.
|
||||
Args:
|
||||
ticker (str): Ticker of a company. e.g. AAPL, TSM
|
||||
curr_date (str): Current date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted string containing the latest fundamental information about the company on the given date.
|
||||
"""
|
||||
|
||||
openai_fundamentals_results = interface.get_fundamentals_openai(
|
||||
ticker, curr_date
|
||||
)
|
||||
|
||||
return openai_fundamentals_results
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
from langchain_core.tools import tool
|
||||
from typing import Annotated
|
||||
from tradingagents.dataflows.interface import route_to_vendor
|
||||
|
||||
|
||||
@tool
|
||||
def get_stock_data(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve stock price data (OHLCV) for a given ticker symbol.
|
||||
Uses the configured core_stock_apis vendor.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the stock price data for the specified ticker symbol in the specified date range.
|
||||
"""
|
||||
return route_to_vendor("get_stock_data", symbol, start_date, end_date)
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
from langchain_core.tools import tool
|
||||
from typing import Annotated
|
||||
from tradingagents.dataflows.interface import route_to_vendor
|
||||
|
||||
|
||||
@tool
|
||||
def get_fundamentals(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve comprehensive fundamental data for a given ticker symbol.
|
||||
Uses the configured fundamental_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
curr_date (str): Current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: A formatted report containing comprehensive fundamental data
|
||||
"""
|
||||
return route_to_vendor("get_fundamentals", ticker, curr_date)
|
||||
|
||||
|
||||
@tool
|
||||
def get_balance_sheet(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve balance sheet data for a given ticker symbol.
|
||||
Uses the configured fundamental_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
freq (str): Reporting frequency: annual/quarterly (default quarterly)
|
||||
curr_date (str): Current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: A formatted report containing balance sheet data
|
||||
"""
|
||||
return route_to_vendor("get_balance_sheet", ticker, freq, curr_date)
|
||||
|
||||
|
||||
@tool
|
||||
def get_cashflow(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve cash flow statement data for a given ticker symbol.
|
||||
Uses the configured fundamental_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
freq (str): Reporting frequency: annual/quarterly (default quarterly)
|
||||
curr_date (str): Current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: A formatted report containing cash flow statement data
|
||||
"""
|
||||
return route_to_vendor("get_cashflow", ticker, freq, curr_date)
|
||||
|
||||
|
||||
@tool
|
||||
def get_income_statement(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve income statement data for a given ticker symbol.
|
||||
Uses the configured fundamental_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
freq (str): Reporting frequency: annual/quarterly (default quarterly)
|
||||
curr_date (str): Current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: A formatted report containing income statement data
|
||||
"""
|
||||
return route_to_vendor("get_income_statement", ticker, freq, curr_date)
|
||||
|
|
@ -1,106 +1,71 @@
|
|||
"""Financial situation memory using BM25 for lexical similarity matching.
|
||||
|
||||
Uses BM25 (Best Matching 25) algorithm for retrieval - no API calls,
|
||||
no token limits, works offline with any LLM provider.
|
||||
"""
|
||||
|
||||
from rank_bm25 import BM25Okapi
|
||||
from typing import List, Tuple
|
||||
import re
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
from openai import OpenAI
|
||||
import numpy as np
|
||||
|
||||
|
||||
class FinancialSituationMemory:
|
||||
"""Memory system for storing and retrieving financial situations using BM25."""
|
||||
def __init__(self, name):
|
||||
self.client = OpenAI()
|
||||
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
|
||||
self.situation_collection = self.chroma_client.create_collection(name=name)
|
||||
|
||||
def __init__(self, name: str, config: dict = None):
|
||||
"""Initialize the memory system.
|
||||
def get_embedding(self, text):
|
||||
"""Get OpenAI embedding for a text"""
|
||||
response = self.client.embeddings.create(
|
||||
model="text-embedding-ada-002", input=text
|
||||
)
|
||||
return response.data[0].embedding
|
||||
|
||||
Args:
|
||||
name: Name identifier for this memory instance
|
||||
config: Configuration dict (kept for API compatibility, not used for BM25)
|
||||
"""
|
||||
self.name = name
|
||||
self.documents: List[str] = []
|
||||
self.recommendations: List[str] = []
|
||||
self.bm25 = None
|
||||
def add_situations(self, situations_and_advice):
|
||||
"""Add financial situations and their corresponding advice. Parameter is a list of tuples (situation, rec)"""
|
||||
|
||||
def _tokenize(self, text: str) -> List[str]:
|
||||
"""Tokenize text for BM25 indexing.
|
||||
situations = []
|
||||
advice = []
|
||||
ids = []
|
||||
embeddings = []
|
||||
|
||||
Simple whitespace + punctuation tokenization with lowercasing.
|
||||
"""
|
||||
# Lowercase and split on non-alphanumeric characters
|
||||
tokens = re.findall(r'\b\w+\b', text.lower())
|
||||
return tokens
|
||||
offset = self.situation_collection.count()
|
||||
|
||||
def _rebuild_index(self):
|
||||
"""Rebuild the BM25 index after adding documents."""
|
||||
if self.documents:
|
||||
tokenized_docs = [self._tokenize(doc) for doc in self.documents]
|
||||
self.bm25 = BM25Okapi(tokenized_docs)
|
||||
else:
|
||||
self.bm25 = None
|
||||
for i, (situation, recommendation) in enumerate(situations_and_advice):
|
||||
situations.append(situation)
|
||||
advice.append(recommendation)
|
||||
ids.append(str(offset + i))
|
||||
embeddings.append(self.get_embedding(situation))
|
||||
|
||||
def add_situations(self, situations_and_advice: List[Tuple[str, str]]):
|
||||
"""Add financial situations and their corresponding advice.
|
||||
self.situation_collection.add(
|
||||
documents=situations,
|
||||
metadatas=[{"recommendation": rec} for rec in advice],
|
||||
embeddings=embeddings,
|
||||
ids=ids,
|
||||
)
|
||||
|
||||
Args:
|
||||
situations_and_advice: List of tuples (situation, recommendation)
|
||||
"""
|
||||
for situation, recommendation in situations_and_advice:
|
||||
self.documents.append(situation)
|
||||
self.recommendations.append(recommendation)
|
||||
def get_memories(self, current_situation, n_matches=1):
|
||||
"""Find matching recommendations using OpenAI embeddings"""
|
||||
query_embedding = self.get_embedding(current_situation)
|
||||
|
||||
# Rebuild BM25 index with new documents
|
||||
self._rebuild_index()
|
||||
results = self.situation_collection.query(
|
||||
query_embeddings=[query_embedding],
|
||||
n_results=n_matches,
|
||||
include=["metadatas", "documents", "distances"],
|
||||
)
|
||||
|
||||
def get_memories(self, current_situation: str, n_matches: int = 1) -> List[dict]:
|
||||
"""Find matching recommendations using BM25 similarity.
|
||||
matched_results = []
|
||||
for i in range(len(results["documents"][0])):
|
||||
matched_results.append(
|
||||
{
|
||||
"matched_situation": results["documents"][0][i],
|
||||
"recommendation": results["metadatas"][0][i]["recommendation"],
|
||||
"similarity_score": 1 - results["distances"][0][i],
|
||||
}
|
||||
)
|
||||
|
||||
Args:
|
||||
current_situation: The current financial situation to match against
|
||||
n_matches: Number of top matches to return
|
||||
|
||||
Returns:
|
||||
List of dicts with matched_situation, recommendation, and similarity_score
|
||||
"""
|
||||
if not self.documents or self.bm25 is None:
|
||||
return []
|
||||
|
||||
# Tokenize query
|
||||
query_tokens = self._tokenize(current_situation)
|
||||
|
||||
# Get BM25 scores for all documents
|
||||
scores = self.bm25.get_scores(query_tokens)
|
||||
|
||||
# Get top-n indices sorted by score (descending)
|
||||
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:n_matches]
|
||||
|
||||
# Build results
|
||||
results = []
|
||||
max_score = float(scores.max()) if len(scores) > 0 and scores.max() > 0 else 1.0
|
||||
|
||||
for idx in top_indices:
|
||||
# Normalize score to 0-1 range for consistency
|
||||
normalized_score = scores[idx] / max_score if max_score > 0 else 0
|
||||
results.append({
|
||||
"matched_situation": self.documents[idx],
|
||||
"recommendation": self.recommendations[idx],
|
||||
"similarity_score": normalized_score,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def clear(self):
|
||||
"""Clear all stored memories."""
|
||||
self.documents = []
|
||||
self.recommendations = []
|
||||
self.bm25 = None
|
||||
return matched_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
matcher = FinancialSituationMemory("test_memory")
|
||||
matcher = FinancialSituationMemory()
|
||||
|
||||
# Example data
|
||||
example_data = [
|
||||
|
|
@ -127,7 +92,7 @@ if __name__ == "__main__":
|
|||
|
||||
# Example query
|
||||
current_situation = """
|
||||
Market showing increased volatility in tech sector, with institutional investors
|
||||
Market showing increased volatility in tech sector, with institutional investors
|
||||
reducing positions and rising interest rates affecting growth stock valuations
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
from langchain_core.tools import tool
|
||||
from typing import Annotated
|
||||
from tradingagents.dataflows.interface import route_to_vendor
|
||||
|
||||
@tool
|
||||
def get_news(
|
||||
ticker: Annotated[str, "Ticker symbol"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve news data for a given ticker symbol.
|
||||
Uses the configured news_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted string containing news data
|
||||
"""
|
||||
return route_to_vendor("get_news", ticker, start_date, end_date)
|
||||
|
||||
@tool
|
||||
def get_global_news(
|
||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "Number of days to look back"] = 7,
|
||||
limit: Annotated[int, "Maximum number of articles to return"] = 5,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve global news data.
|
||||
Uses the configured news_data vendor.
|
||||
Args:
|
||||
curr_date (str): Current date in yyyy-mm-dd format
|
||||
look_back_days (int): Number of days to look back (default 7)
|
||||
limit (int): Maximum number of articles to return (default 5)
|
||||
Returns:
|
||||
str: A formatted string containing global news data
|
||||
"""
|
||||
return route_to_vendor("get_global_news", curr_date, look_back_days, limit)
|
||||
|
||||
@tool
|
||||
def get_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve insider transaction information about a company.
|
||||
Uses the configured news_data vendor.
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
Returns:
|
||||
str: A report of insider transaction data
|
||||
"""
|
||||
return route_to_vendor("get_insider_transactions", ticker)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
from langchain_core.tools import tool
|
||||
from typing import Annotated
|
||||
from tradingagents.dataflows.interface import route_to_vendor
|
||||
|
||||
@tool
|
||||
def get_indicators(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||
curr_date: Annotated[str, "The current trading date you are trading on, YYYY-mm-dd"],
|
||||
look_back_days: Annotated[int, "how many days to look back"] = 30,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve a single technical indicator for a given ticker symbol.
|
||||
Uses the configured technical_indicators vendor.
|
||||
Args:
|
||||
symbol (str): Ticker symbol of the company, e.g. AAPL, TSM
|
||||
indicator (str): A single technical indicator name, e.g. 'rsi', 'macd'. Call this tool once per indicator.
|
||||
curr_date (str): The current trading date you are trading on, YYYY-mm-dd
|
||||
look_back_days (int): How many days to look back, default is 30
|
||||
Returns:
|
||||
str: A formatted dataframe containing the technical indicators for the specified ticker symbol and indicator.
|
||||
"""
|
||||
# LLMs sometimes pass multiple indicators as a comma-separated string;
|
||||
# split and process each individually.
|
||||
indicators = [i.strip().lower() for i in indicator.split(",") if i.strip()]
|
||||
results = []
|
||||
for ind in indicators:
|
||||
try:
|
||||
results.append(route_to_vendor("get_indicators", symbol, ind, curr_date, look_back_days))
|
||||
except ValueError as e:
|
||||
results.append(str(e))
|
||||
return "\n\n".join(results)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from .finnhub_utils import get_data_in_range
|
||||
from .googlenews_utils import getNewsData
|
||||
from .yfin_utils import YFinanceUtils
|
||||
from .reddit_utils import fetch_top_from_category
|
||||
from .stockstats_utils import StockstatsUtils
|
||||
from .yfin_utils import YFinanceUtils
|
||||
|
||||
from .interface import (
|
||||
# News and sentiment functions
|
||||
get_finnhub_news,
|
||||
get_finnhub_company_insider_sentiment,
|
||||
get_finnhub_company_insider_transactions,
|
||||
get_google_news,
|
||||
get_reddit_global_news,
|
||||
get_reddit_company_news,
|
||||
# Financial statements functions
|
||||
get_simfin_balance_sheet,
|
||||
get_simfin_cashflow,
|
||||
get_simfin_income_statements,
|
||||
# Technical analysis functions
|
||||
get_stock_stats_indicators_window,
|
||||
get_stockstats_indicator,
|
||||
# Market data functions
|
||||
get_YFin_data_window,
|
||||
get_YFin_data,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# News and sentiment functions
|
||||
"get_finnhub_news",
|
||||
"get_finnhub_company_insider_sentiment",
|
||||
"get_finnhub_company_insider_transactions",
|
||||
"get_google_news",
|
||||
"get_reddit_global_news",
|
||||
"get_reddit_company_news",
|
||||
# Financial statements functions
|
||||
"get_simfin_balance_sheet",
|
||||
"get_simfin_cashflow",
|
||||
"get_simfin_income_statements",
|
||||
# Technical analysis functions
|
||||
"get_stock_stats_indicators_window",
|
||||
"get_stockstats_indicator",
|
||||
# Market data functions
|
||||
"get_YFin_data_window",
|
||||
"get_YFin_data",
|
||||
]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Import functions from specialized modules
|
||||
from .alpha_vantage_stock import get_stock
|
||||
from .alpha_vantage_indicator import get_indicator
|
||||
from .alpha_vantage_fundamentals import get_fundamentals, get_balance_sheet, get_cashflow, get_income_statement
|
||||
from .alpha_vantage_news import get_news, get_global_news, get_insider_transactions
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import os
|
||||
import requests
|
||||
import pandas as pd
|
||||
import json
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
API_BASE_URL = "https://www.alphavantage.co/query"
|
||||
|
||||
def get_api_key() -> str:
|
||||
"""Retrieve the API key for Alpha Vantage from environment variables."""
|
||||
api_key = os.getenv("ALPHA_VANTAGE_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("ALPHA_VANTAGE_API_KEY environment variable is not set.")
|
||||
return api_key
|
||||
|
||||
def format_datetime_for_api(date_input) -> str:
|
||||
"""Convert various date formats to YYYYMMDDTHHMM format required by Alpha Vantage API."""
|
||||
if isinstance(date_input, str):
|
||||
# If already in correct format, return as-is
|
||||
if len(date_input) == 13 and 'T' in date_input:
|
||||
return date_input
|
||||
# Try to parse common date formats
|
||||
try:
|
||||
dt = datetime.strptime(date_input, "%Y-%m-%d")
|
||||
return dt.strftime("%Y%m%dT0000")
|
||||
except ValueError:
|
||||
try:
|
||||
dt = datetime.strptime(date_input, "%Y-%m-%d %H:%M")
|
||||
return dt.strftime("%Y%m%dT%H%M")
|
||||
except ValueError:
|
||||
raise ValueError(f"Unsupported date format: {date_input}")
|
||||
elif isinstance(date_input, datetime):
|
||||
return date_input.strftime("%Y%m%dT%H%M")
|
||||
else:
|
||||
raise ValueError(f"Date must be string or datetime object, got {type(date_input)}")
|
||||
|
||||
class AlphaVantageRateLimitError(Exception):
|
||||
"""Exception raised when Alpha Vantage API rate limit is exceeded."""
|
||||
pass
|
||||
|
||||
def _make_api_request(function_name: str, params: dict) -> dict | str:
|
||||
"""Helper function to make API requests and handle responses.
|
||||
|
||||
Raises:
|
||||
AlphaVantageRateLimitError: When API rate limit is exceeded
|
||||
"""
|
||||
# Create a copy of params to avoid modifying the original
|
||||
api_params = params.copy()
|
||||
api_params.update({
|
||||
"function": function_name,
|
||||
"apikey": get_api_key(),
|
||||
"source": "trading_agents",
|
||||
})
|
||||
|
||||
# Handle entitlement parameter if present in params or global variable
|
||||
current_entitlement = globals().get('_current_entitlement')
|
||||
entitlement = api_params.get("entitlement") or current_entitlement
|
||||
|
||||
if entitlement:
|
||||
api_params["entitlement"] = entitlement
|
||||
elif "entitlement" in api_params:
|
||||
# Remove entitlement if it's None or empty
|
||||
api_params.pop("entitlement", None)
|
||||
|
||||
response = requests.get(API_BASE_URL, params=api_params)
|
||||
response.raise_for_status()
|
||||
|
||||
response_text = response.text
|
||||
|
||||
# Check if response is JSON (error responses are typically JSON)
|
||||
try:
|
||||
response_json = json.loads(response_text)
|
||||
# Check for rate limit error
|
||||
if "Information" in response_json:
|
||||
info_message = response_json["Information"]
|
||||
if "rate limit" in info_message.lower() or "api key" in info_message.lower():
|
||||
raise AlphaVantageRateLimitError(f"Alpha Vantage rate limit exceeded: {info_message}")
|
||||
except json.JSONDecodeError:
|
||||
# Response is not JSON (likely CSV data), which is normal
|
||||
pass
|
||||
|
||||
return response_text
|
||||
|
||||
|
||||
|
||||
def _filter_csv_by_date_range(csv_data: str, start_date: str, end_date: str) -> str:
|
||||
"""
|
||||
Filter CSV data to include only rows within the specified date range.
|
||||
|
||||
Args:
|
||||
csv_data: CSV string from Alpha Vantage API
|
||||
start_date: Start date in yyyy-mm-dd format
|
||||
end_date: End date in yyyy-mm-dd format
|
||||
|
||||
Returns:
|
||||
Filtered CSV string
|
||||
"""
|
||||
if not csv_data or csv_data.strip() == "":
|
||||
return csv_data
|
||||
|
||||
try:
|
||||
# Parse CSV data
|
||||
df = pd.read_csv(StringIO(csv_data))
|
||||
|
||||
# Assume the first column is the date column (timestamp)
|
||||
date_col = df.columns[0]
|
||||
df[date_col] = pd.to_datetime(df[date_col])
|
||||
|
||||
# Filter by date range
|
||||
start_dt = pd.to_datetime(start_date)
|
||||
end_dt = pd.to_datetime(end_date)
|
||||
|
||||
filtered_df = df[(df[date_col] >= start_dt) & (df[date_col] <= end_dt)]
|
||||
|
||||
# Convert back to CSV string
|
||||
return filtered_df.to_csv(index=False)
|
||||
|
||||
except Exception as e:
|
||||
# If filtering fails, return original data with a warning
|
||||
print(f"Warning: Failed to filter CSV data by date range: {e}")
|
||||
return csv_data
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
from .alpha_vantage_common import _make_api_request
|
||||
|
||||
|
||||
def _filter_reports_by_date(result, curr_date: str):
|
||||
"""Filter annualReports/quarterlyReports to exclude entries after curr_date.
|
||||
|
||||
Prevents look-ahead bias by removing fiscal periods that end after
|
||||
the simulation's current date.
|
||||
"""
|
||||
if not curr_date or not isinstance(result, dict):
|
||||
return result
|
||||
for key in ("annualReports", "quarterlyReports"):
|
||||
if key in result:
|
||||
result[key] = [
|
||||
r for r in result[key]
|
||||
if r.get("fiscalDateEnding", "") <= curr_date
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
||||
"""
|
||||
Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage.
|
||||
|
||||
Args:
|
||||
ticker (str): Ticker symbol of the company
|
||||
curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage)
|
||||
|
||||
Returns:
|
||||
str: Company overview data including financial ratios and key metrics
|
||||
"""
|
||||
params = {
|
||||
"symbol": ticker,
|
||||
}
|
||||
|
||||
return _make_api_request("OVERVIEW", params)
|
||||
|
||||
|
||||
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
||||
"""Retrieve balance sheet data for a given ticker symbol using Alpha Vantage."""
|
||||
result = _make_api_request("BALANCE_SHEET", {"symbol": ticker})
|
||||
return _filter_reports_by_date(result, curr_date)
|
||||
|
||||
|
||||
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
||||
"""Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage."""
|
||||
result = _make_api_request("CASH_FLOW", {"symbol": ticker})
|
||||
return _filter_reports_by_date(result, curr_date)
|
||||
|
||||
|
||||
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
||||
"""Retrieve income statement data for a given ticker symbol using Alpha Vantage."""
|
||||
result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker})
|
||||
return _filter_reports_by_date(result, curr_date)
|
||||
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
from .alpha_vantage_common import _make_api_request
|
||||
|
||||
def get_indicator(
|
||||
symbol: str,
|
||||
indicator: str,
|
||||
curr_date: str,
|
||||
look_back_days: int,
|
||||
interval: str = "daily",
|
||||
time_period: int = 14,
|
||||
series_type: str = "close"
|
||||
) -> str:
|
||||
"""
|
||||
Returns Alpha Vantage technical indicator values over a time window.
|
||||
|
||||
Args:
|
||||
symbol: ticker symbol of the company
|
||||
indicator: technical indicator to get the analysis and report of
|
||||
curr_date: The current trading date you are trading on, YYYY-mm-dd
|
||||
look_back_days: how many days to look back
|
||||
interval: Time interval (daily, weekly, monthly)
|
||||
time_period: Number of data points for calculation
|
||||
series_type: The desired price type (close, open, high, low)
|
||||
|
||||
Returns:
|
||||
String containing indicator values and description
|
||||
"""
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
supported_indicators = {
|
||||
"close_50_sma": ("50 SMA", "close"),
|
||||
"close_200_sma": ("200 SMA", "close"),
|
||||
"close_10_ema": ("10 EMA", "close"),
|
||||
"macd": ("MACD", "close"),
|
||||
"macds": ("MACD Signal", "close"),
|
||||
"macdh": ("MACD Histogram", "close"),
|
||||
"rsi": ("RSI", "close"),
|
||||
"boll": ("Bollinger Middle", "close"),
|
||||
"boll_ub": ("Bollinger Upper Band", "close"),
|
||||
"boll_lb": ("Bollinger Lower Band", "close"),
|
||||
"atr": ("ATR", None),
|
||||
"vwma": ("VWMA", "close")
|
||||
}
|
||||
|
||||
indicator_descriptions = {
|
||||
"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.",
|
||||
"close_200_sma": "200 SMA: A long-term trend benchmark. Usage: Confirm overall market trend and identify golden/death cross setups. Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries.",
|
||||
"close_10_ema": "10 EMA: A responsive short-term average. Usage: Capture quick shifts in momentum and potential entry points. Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals.",
|
||||
"macd": "MACD: Computes momentum via differences of EMAs. Usage: Look for crossovers and divergence as signals of trend changes. Tips: Confirm with other indicators in low-volatility or sideways markets.",
|
||||
"macds": "MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers with the MACD line to trigger trades. Tips: Should be part of a broader strategy to avoid false positives.",
|
||||
"macdh": "MACD Histogram: Shows the gap between the MACD line and its signal. Usage: Visualize momentum strength and spot divergence early. Tips: Can be volatile; complement with additional filters in fast-moving markets.",
|
||||
"rsi": "RSI: Measures momentum to flag overbought/oversold conditions. Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis.",
|
||||
"boll": "Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals.",
|
||||
"boll_ub": "Bollinger Upper Band: Typically 2 standard deviations above the middle line. Usage: Signals potential overbought conditions and breakout zones. Tips: Confirm signals with other tools; prices may ride the band in strong trends.",
|
||||
"boll_lb": "Bollinger Lower Band: Typically 2 standard deviations below the middle line. Usage: Indicates potential oversold conditions. Tips: Use additional analysis to avoid false reversal signals.",
|
||||
"atr": "ATR: Averages true range to measure volatility. Usage: Set stop-loss levels and adjust position sizes based on current market volatility. Tips: It's a reactive measure, so use it as part of a broader risk management strategy.",
|
||||
"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."
|
||||
}
|
||||
|
||||
if indicator not in supported_indicators:
|
||||
raise ValueError(
|
||||
f"Indicator {indicator} is not supported. Please choose from: {list(supported_indicators.keys())}"
|
||||
)
|
||||
|
||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = curr_date_dt - relativedelta(days=look_back_days)
|
||||
|
||||
# Get the full data for the period instead of making individual calls
|
||||
_, required_series_type = supported_indicators[indicator]
|
||||
|
||||
# Use the provided series_type or fall back to the required one
|
||||
if required_series_type:
|
||||
series_type = required_series_type
|
||||
|
||||
try:
|
||||
# Get indicator data for the period
|
||||
if indicator == "close_50_sma":
|
||||
data = _make_api_request("SMA", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": "50",
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "close_200_sma":
|
||||
data = _make_api_request("SMA", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": "200",
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "close_10_ema":
|
||||
data = _make_api_request("EMA", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": "10",
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "macd":
|
||||
data = _make_api_request("MACD", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "macds":
|
||||
data = _make_api_request("MACD", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "macdh":
|
||||
data = _make_api_request("MACD", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "rsi":
|
||||
data = _make_api_request("RSI", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": str(time_period),
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator in ["boll", "boll_ub", "boll_lb"]:
|
||||
data = _make_api_request("BBANDS", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": "20",
|
||||
"series_type": series_type,
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "atr":
|
||||
data = _make_api_request("ATR", {
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"time_period": str(time_period),
|
||||
"datatype": "csv"
|
||||
})
|
||||
elif indicator == "vwma":
|
||||
# Alpha Vantage doesn't have direct VWMA, so we'll return an informative message
|
||||
# In a real implementation, this would need to be calculated from OHLCV data
|
||||
return f"## VWMA (Volume Weighted Moving Average) for {symbol}:\n\nVWMA calculation requires OHLCV data and is not directly available from Alpha Vantage API.\nThis indicator would need to be calculated from the raw stock data using volume-weighted price averaging.\n\n{indicator_descriptions.get('vwma', 'No description available.')}"
|
||||
else:
|
||||
return f"Error: Indicator {indicator} not implemented yet."
|
||||
|
||||
# Parse CSV data and extract values for the date range
|
||||
lines = data.strip().split('\n')
|
||||
if len(lines) < 2:
|
||||
return f"Error: No data returned for {indicator}"
|
||||
|
||||
# Parse header and data
|
||||
header = [col.strip() for col in lines[0].split(',')]
|
||||
try:
|
||||
date_col_idx = header.index('time')
|
||||
except ValueError:
|
||||
return f"Error: 'time' column not found in data for {indicator}. Available columns: {header}"
|
||||
|
||||
# Map internal indicator names to expected CSV column names from Alpha Vantage
|
||||
col_name_map = {
|
||||
"macd": "MACD", "macds": "MACD_Signal", "macdh": "MACD_Hist",
|
||||
"boll": "Real Middle Band", "boll_ub": "Real Upper Band", "boll_lb": "Real Lower Band",
|
||||
"rsi": "RSI", "atr": "ATR", "close_10_ema": "EMA",
|
||||
"close_50_sma": "SMA", "close_200_sma": "SMA"
|
||||
}
|
||||
|
||||
target_col_name = col_name_map.get(indicator)
|
||||
|
||||
if not target_col_name:
|
||||
# Default to the second column if no specific mapping exists
|
||||
value_col_idx = 1
|
||||
else:
|
||||
try:
|
||||
value_col_idx = header.index(target_col_name)
|
||||
except ValueError:
|
||||
return f"Error: Column '{target_col_name}' not found for indicator '{indicator}'. Available columns: {header}"
|
||||
|
||||
result_data = []
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
values = line.split(',')
|
||||
if len(values) > value_col_idx:
|
||||
try:
|
||||
date_str = values[date_col_idx].strip()
|
||||
# Parse the date
|
||||
date_dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
|
||||
# Check if date is in our range
|
||||
if before <= date_dt <= curr_date_dt:
|
||||
value = values[value_col_idx].strip()
|
||||
result_data.append((date_dt, value))
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
# Sort by date and format output
|
||||
result_data.sort(key=lambda x: x[0])
|
||||
|
||||
ind_string = ""
|
||||
for date_dt, value in result_data:
|
||||
ind_string += f"{date_dt.strftime('%Y-%m-%d')}: {value}\n"
|
||||
|
||||
if not ind_string:
|
||||
ind_string = "No data available for the specified date range.\n"
|
||||
|
||||
result_str = (
|
||||
f"## {indicator.upper()} values from {before.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
|
||||
+ ind_string
|
||||
+ "\n\n"
|
||||
+ indicator_descriptions.get(indicator, "No description available.")
|
||||
)
|
||||
|
||||
return result_str
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting Alpha Vantage indicator data for {indicator}: {e}")
|
||||
return f"Error retrieving {indicator} data: {str(e)}"
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
from .alpha_vantage_common import _make_api_request, format_datetime_for_api
|
||||
|
||||
def get_news(ticker, start_date, end_date) -> dict[str, str] | str:
|
||||
"""Returns live and historical market news & sentiment data from premier news outlets worldwide.
|
||||
|
||||
Covers stocks, cryptocurrencies, forex, and topics like fiscal policy, mergers & acquisitions, IPOs.
|
||||
|
||||
Args:
|
||||
ticker: Stock symbol for news articles.
|
||||
start_date: Start date for news search.
|
||||
end_date: End date for news search.
|
||||
|
||||
Returns:
|
||||
Dictionary containing news sentiment data or JSON string.
|
||||
"""
|
||||
|
||||
params = {
|
||||
"tickers": ticker,
|
||||
"time_from": format_datetime_for_api(start_date),
|
||||
"time_to": format_datetime_for_api(end_date),
|
||||
}
|
||||
|
||||
return _make_api_request("NEWS_SENTIMENT", params)
|
||||
|
||||
def get_global_news(curr_date, look_back_days: int = 7, limit: int = 50) -> dict[str, str] | str:
|
||||
"""Returns global market news & sentiment data without ticker-specific filtering.
|
||||
|
||||
Covers broad market topics like financial markets, economy, and more.
|
||||
|
||||
Args:
|
||||
curr_date: Current date in yyyy-mm-dd format.
|
||||
look_back_days: Number of days to look back (default 7).
|
||||
limit: Maximum number of articles (default 50).
|
||||
|
||||
Returns:
|
||||
Dictionary containing global news sentiment data or JSON string.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Calculate start date
|
||||
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
start_dt = curr_dt - timedelta(days=look_back_days)
|
||||
start_date = start_dt.strftime("%Y-%m-%d")
|
||||
|
||||
params = {
|
||||
"topics": "financial_markets,economy_macro,economy_monetary",
|
||||
"time_from": format_datetime_for_api(start_date),
|
||||
"time_to": format_datetime_for_api(curr_date),
|
||||
"limit": str(limit),
|
||||
}
|
||||
|
||||
return _make_api_request("NEWS_SENTIMENT", params)
|
||||
|
||||
|
||||
def get_insider_transactions(symbol: str) -> dict[str, str] | str:
|
||||
"""Returns latest and historical insider transactions by key stakeholders.
|
||||
|
||||
Covers transactions by founders, executives, board members, etc.
|
||||
|
||||
Args:
|
||||
symbol: Ticker symbol. Example: "IBM".
|
||||
|
||||
Returns:
|
||||
Dictionary containing insider transaction data or JSON string.
|
||||
"""
|
||||
|
||||
params = {
|
||||
"symbol": symbol,
|
||||
}
|
||||
|
||||
return _make_api_request("INSIDER_TRANSACTIONS", params)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
from datetime import datetime
|
||||
from .alpha_vantage_common import _make_api_request, _filter_csv_by_date_range
|
||||
|
||||
def get_stock(
|
||||
symbol: str,
|
||||
start_date: str,
|
||||
end_date: str
|
||||
) -> str:
|
||||
"""
|
||||
Returns raw daily OHLCV values, adjusted close values, and historical split/dividend events
|
||||
filtered to the specified date range.
|
||||
|
||||
Args:
|
||||
symbol: The name of the equity. For example: symbol=IBM
|
||||
start_date: Start date in yyyy-mm-dd format
|
||||
end_date: End date in yyyy-mm-dd format
|
||||
|
||||
Returns:
|
||||
CSV string containing the daily adjusted time series data filtered to the date range.
|
||||
"""
|
||||
# Parse dates to determine the range
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
today = datetime.now()
|
||||
|
||||
# Choose outputsize based on whether the requested range is within the latest 100 days
|
||||
# Compact returns latest 100 data points, so check if start_date is recent enough
|
||||
days_from_today_to_start = (today - start_dt).days
|
||||
outputsize = "compact" if days_from_today_to_start < 100 else "full"
|
||||
|
||||
params = {
|
||||
"symbol": symbol,
|
||||
"outputsize": outputsize,
|
||||
"datatype": "csv",
|
||||
}
|
||||
|
||||
response = _make_api_request("TIME_SERIES_DAILY_ADJUSTED", params)
|
||||
|
||||
return _filter_csv_by_date_range(response, start_date, end_date)
|
||||
|
|
@ -3,21 +3,24 @@ from typing import Dict, Optional
|
|||
|
||||
# Use default config but allow it to be overridden
|
||||
_config: Optional[Dict] = None
|
||||
DATA_DIR: Optional[str] = None
|
||||
|
||||
|
||||
def initialize_config():
|
||||
"""Initialize the configuration with default values."""
|
||||
global _config
|
||||
global _config, DATA_DIR
|
||||
if _config is None:
|
||||
_config = default_config.DEFAULT_CONFIG.copy()
|
||||
DATA_DIR = _config["data_dir"]
|
||||
|
||||
|
||||
def set_config(config: Dict):
|
||||
"""Update the configuration with custom values."""
|
||||
global _config
|
||||
global _config, DATA_DIR
|
||||
if _config is None:
|
||||
_config = default_config.DEFAULT_CONFIG.copy()
|
||||
_config.update(config)
|
||||
DATA_DIR = _config["data_dir"]
|
||||
|
||||
|
||||
def get_config() -> Dict:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
|
||||
def get_data_in_range(ticker, start_date, end_date, data_type, data_dir, period=None):
|
||||
"""
|
||||
Gets finnhub data saved and processed on disk.
|
||||
Args:
|
||||
start_date (str): Start date in YYYY-MM-DD format.
|
||||
end_date (str): End date in YYYY-MM-DD format.
|
||||
data_type (str): Type of data from finnhub to fetch. Can be insider_trans, SEC_filings, news_data, insider_senti, or fin_as_reported.
|
||||
data_dir (str): Directory where the data is saved.
|
||||
period (str): Default to none, if there is a period specified, should be annual or quarterly.
|
||||
"""
|
||||
|
||||
if period:
|
||||
data_path = os.path.join(
|
||||
data_dir,
|
||||
"finnhub_data",
|
||||
data_type,
|
||||
f"{ticker}_{period}_data_formatted.json",
|
||||
)
|
||||
else:
|
||||
data_path = os.path.join(
|
||||
data_dir, "finnhub_data", data_type, f"{ticker}_data_formatted.json"
|
||||
)
|
||||
|
||||
data = open(data_path, "r")
|
||||
data = json.load(data)
|
||||
|
||||
# filter keys (date, str in format YYYY-MM-DD) by the date range (str, str in format YYYY-MM-DD)
|
||||
filtered_data = {}
|
||||
for key, value in data.items():
|
||||
if start_date <= key <= end_date and len(value) > 0:
|
||||
filtered_data[key] = value
|
||||
return filtered_data
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import json
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
import time
|
||||
import random
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
retry_if_exception_type,
|
||||
retry_if_result,
|
||||
)
|
||||
|
||||
|
||||
def is_rate_limited(response):
|
||||
"""Check if the response indicates rate limiting (status code 429)"""
|
||||
return response.status_code == 429
|
||||
|
||||
|
||||
@retry(
|
||||
retry=(retry_if_result(is_rate_limited)),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
stop=stop_after_attempt(5),
|
||||
)
|
||||
def make_request(url, headers):
|
||||
"""Make a request with retry logic for rate limiting"""
|
||||
# Random delay before each request to avoid detection
|
||||
time.sleep(random.uniform(2, 6))
|
||||
response = requests.get(url, headers=headers)
|
||||
return response
|
||||
|
||||
|
||||
def getNewsData(query, start_date, end_date):
|
||||
"""
|
||||
Scrape Google News search results for a given query and date range.
|
||||
query: str - search query
|
||||
start_date: str - start date in the format yyyy-mm-dd or mm/dd/yyyy
|
||||
end_date: str - end date in the format yyyy-mm-dd or mm/dd/yyyy
|
||||
"""
|
||||
if "-" in start_date:
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
start_date = start_date.strftime("%m/%d/%Y")
|
||||
if "-" in end_date:
|
||||
end_date = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
end_date = end_date.strftime("%m/%d/%Y")
|
||||
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/101.0.4951.54 Safari/537.36"
|
||||
)
|
||||
}
|
||||
|
||||
news_results = []
|
||||
page = 0
|
||||
while True:
|
||||
offset = page * 10
|
||||
url = (
|
||||
f"https://www.google.com/search?q={query}"
|
||||
f"&tbs=cdr:1,cd_min:{start_date},cd_max:{end_date}"
|
||||
f"&tbm=nws&start={offset}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = make_request(url, headers)
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
results_on_page = soup.select("div.SoaBEf")
|
||||
|
||||
if not results_on_page:
|
||||
break # No more results found
|
||||
|
||||
for el in results_on_page:
|
||||
try:
|
||||
link = el.find("a")["href"]
|
||||
title = el.select_one("div.MBeuO").get_text()
|
||||
snippet = el.select_one(".GI74Re").get_text()
|
||||
date = el.select_one(".LfVVr").get_text()
|
||||
source = el.select_one(".NUnG9d span").get_text()
|
||||
news_results.append(
|
||||
{
|
||||
"link": link,
|
||||
"title": title,
|
||||
"snippet": snippet,
|
||||
"date": date,
|
||||
"source": source,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error processing result: {e}")
|
||||
# If one of the fields is not found, skip this result
|
||||
continue
|
||||
|
||||
# Update the progress bar with the current count of results scraped
|
||||
|
||||
# Check for the "Next" link (pagination)
|
||||
next_link = soup.find("a", id="pnnext")
|
||||
if not next_link:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed after multiple retries: {e}")
|
||||
break
|
||||
|
||||
return news_results
|
||||
|
|
@ -1,162 +1,804 @@
|
|||
from typing import Annotated
|
||||
from typing import Annotated, Dict
|
||||
from .reddit_utils import fetch_top_from_category
|
||||
from .yfin_utils import *
|
||||
from .stockstats_utils import *
|
||||
from .googlenews_utils import *
|
||||
from .finnhub_utils import get_data_in_range
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
import yfinance as yf
|
||||
from openai import OpenAI
|
||||
from .config import get_config, set_config, DATA_DIR
|
||||
|
||||
# Import from vendor-specific modules
|
||||
from .y_finance import (
|
||||
get_YFin_data_online,
|
||||
get_stock_stats_indicators_window,
|
||||
get_fundamentals as get_yfinance_fundamentals,
|
||||
get_balance_sheet as get_yfinance_balance_sheet,
|
||||
get_cashflow as get_yfinance_cashflow,
|
||||
get_income_statement as get_yfinance_income_statement,
|
||||
get_insider_transactions as get_yfinance_insider_transactions,
|
||||
)
|
||||
from .yfinance_news import get_news_yfinance, get_global_news_yfinance
|
||||
from .alpha_vantage import (
|
||||
get_stock as get_alpha_vantage_stock,
|
||||
get_indicator as get_alpha_vantage_indicator,
|
||||
get_fundamentals as get_alpha_vantage_fundamentals,
|
||||
get_balance_sheet as get_alpha_vantage_balance_sheet,
|
||||
get_cashflow as get_alpha_vantage_cashflow,
|
||||
get_income_statement as get_alpha_vantage_income_statement,
|
||||
get_insider_transactions as get_alpha_vantage_insider_transactions,
|
||||
get_news as get_alpha_vantage_news,
|
||||
get_global_news as get_alpha_vantage_global_news,
|
||||
)
|
||||
from .alpha_vantage_common import AlphaVantageRateLimitError
|
||||
|
||||
# Configuration and routing logic
|
||||
from .config import get_config
|
||||
|
||||
# Tools organized by category
|
||||
TOOLS_CATEGORIES = {
|
||||
"core_stock_apis": {
|
||||
"description": "OHLCV stock price data",
|
||||
"tools": [
|
||||
"get_stock_data"
|
||||
]
|
||||
},
|
||||
"technical_indicators": {
|
||||
"description": "Technical analysis indicators",
|
||||
"tools": [
|
||||
"get_indicators"
|
||||
]
|
||||
},
|
||||
"fundamental_data": {
|
||||
"description": "Company fundamentals",
|
||||
"tools": [
|
||||
"get_fundamentals",
|
||||
"get_balance_sheet",
|
||||
"get_cashflow",
|
||||
"get_income_statement"
|
||||
]
|
||||
},
|
||||
"news_data": {
|
||||
"description": "News and insider data",
|
||||
"tools": [
|
||||
"get_news",
|
||||
"get_global_news",
|
||||
"get_insider_transactions",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
VENDOR_LIST = [
|
||||
"yfinance",
|
||||
"alpha_vantage",
|
||||
]
|
||||
|
||||
# Mapping of methods to their vendor-specific implementations
|
||||
VENDOR_METHODS = {
|
||||
# core_stock_apis
|
||||
"get_stock_data": {
|
||||
"alpha_vantage": get_alpha_vantage_stock,
|
||||
"yfinance": get_YFin_data_online,
|
||||
},
|
||||
# technical_indicators
|
||||
"get_indicators": {
|
||||
"alpha_vantage": get_alpha_vantage_indicator,
|
||||
"yfinance": get_stock_stats_indicators_window,
|
||||
},
|
||||
# fundamental_data
|
||||
"get_fundamentals": {
|
||||
"alpha_vantage": get_alpha_vantage_fundamentals,
|
||||
"yfinance": get_yfinance_fundamentals,
|
||||
},
|
||||
"get_balance_sheet": {
|
||||
"alpha_vantage": get_alpha_vantage_balance_sheet,
|
||||
"yfinance": get_yfinance_balance_sheet,
|
||||
},
|
||||
"get_cashflow": {
|
||||
"alpha_vantage": get_alpha_vantage_cashflow,
|
||||
"yfinance": get_yfinance_cashflow,
|
||||
},
|
||||
"get_income_statement": {
|
||||
"alpha_vantage": get_alpha_vantage_income_statement,
|
||||
"yfinance": get_yfinance_income_statement,
|
||||
},
|
||||
# news_data
|
||||
"get_news": {
|
||||
"alpha_vantage": get_alpha_vantage_news,
|
||||
"yfinance": get_news_yfinance,
|
||||
},
|
||||
"get_global_news": {
|
||||
"yfinance": get_global_news_yfinance,
|
||||
"alpha_vantage": get_alpha_vantage_global_news,
|
||||
},
|
||||
"get_insider_transactions": {
|
||||
"alpha_vantage": get_alpha_vantage_insider_transactions,
|
||||
"yfinance": get_yfinance_insider_transactions,
|
||||
},
|
||||
}
|
||||
|
||||
def get_category_for_method(method: str) -> str:
|
||||
"""Get the category that contains the specified method."""
|
||||
for category, info in TOOLS_CATEGORIES.items():
|
||||
if method in info["tools"]:
|
||||
return category
|
||||
raise ValueError(f"Method '{method}' not found in any category")
|
||||
|
||||
def get_vendor(category: str, method: str = None) -> str:
|
||||
"""Get the configured vendor for a data category or specific tool method.
|
||||
Tool-level configuration takes precedence over category-level.
|
||||
def get_finnhub_news(
|
||||
ticker: Annotated[
|
||||
str,
|
||||
"Search query of a company's, e.g. 'AAPL, TSM, etc.",
|
||||
],
|
||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
):
|
||||
"""
|
||||
config = get_config()
|
||||
Retrieve news about a company within a time frame
|
||||
|
||||
# Check tool-level configuration first (if method provided)
|
||||
if method:
|
||||
tool_vendors = config.get("tool_vendors", {})
|
||||
if method in tool_vendors:
|
||||
return tool_vendors[method]
|
||||
Args
|
||||
ticker (str): ticker for the company you are interested in
|
||||
start_date (str): Start date in yyyy-mm-dd format
|
||||
end_date (str): End date in yyyy-mm-dd format
|
||||
Returns
|
||||
str: dataframe containing the news of the company in the time frame
|
||||
|
||||
# Fall back to category-level configuration
|
||||
return config.get("data_vendors", {}).get(category, "default")
|
||||
"""
|
||||
|
||||
def route_to_vendor(method: str, *args, **kwargs):
|
||||
"""Route method calls to appropriate vendor implementation with fallback support."""
|
||||
category = get_category_for_method(method)
|
||||
vendor_config = get_vendor(category, method)
|
||||
primary_vendors = [v.strip() for v in vendor_config.split(',')]
|
||||
start_date = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = start_date - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
if method not in VENDOR_METHODS:
|
||||
raise ValueError(f"Method '{method}' not supported")
|
||||
result = get_data_in_range(ticker, before, curr_date, "news_data", DATA_DIR)
|
||||
|
||||
# Build fallback chain: primary vendors first, then remaining available vendors
|
||||
all_available_vendors = list(VENDOR_METHODS[method].keys())
|
||||
fallback_vendors = primary_vendors.copy()
|
||||
for vendor in all_available_vendors:
|
||||
if vendor not in fallback_vendors:
|
||||
fallback_vendors.append(vendor)
|
||||
if len(result) == 0:
|
||||
return ""
|
||||
|
||||
for vendor in fallback_vendors:
|
||||
if vendor not in VENDOR_METHODS[method]:
|
||||
combined_result = ""
|
||||
for day, data in result.items():
|
||||
if len(data) == 0:
|
||||
continue
|
||||
for entry in data:
|
||||
current_news = (
|
||||
"### " + entry["headline"] + f" ({day})" + "\n" + entry["summary"]
|
||||
)
|
||||
combined_result += current_news + "\n\n"
|
||||
|
||||
vendor_impl = VENDOR_METHODS[method][vendor]
|
||||
impl_func = vendor_impl[0] if isinstance(vendor_impl, list) else vendor_impl
|
||||
return f"## {ticker} News, from {before} to {curr_date}:\n" + str(combined_result)
|
||||
|
||||
try:
|
||||
return impl_func(*args, **kwargs)
|
||||
except AlphaVantageRateLimitError:
|
||||
continue # Only rate limits trigger fallback
|
||||
|
||||
raise RuntimeError(f"No available vendor for '{method}'")
|
||||
def get_finnhub_company_insider_sentiment(
|
||||
ticker: Annotated[str, "ticker symbol for the company"],
|
||||
curr_date: Annotated[
|
||||
str,
|
||||
"current date of you are trading at, yyyy-mm-dd",
|
||||
],
|
||||
look_back_days: Annotated[int, "number of days to look back"],
|
||||
):
|
||||
"""
|
||||
Retrieve insider sentiment about a company (retrieved from public SEC information) for the past 15 days
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
curr_date (str): current date you are trading on, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the sentiment in the past 15 days starting at curr_date
|
||||
"""
|
||||
|
||||
date_obj = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = date_obj - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
data = get_data_in_range(ticker, before, curr_date, "insider_senti", DATA_DIR)
|
||||
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
|
||||
result_str = ""
|
||||
seen_dicts = []
|
||||
for date, senti_list in data.items():
|
||||
for entry in senti_list:
|
||||
if entry not in seen_dicts:
|
||||
result_str += f"### {entry['year']}-{entry['month']}:\nChange: {entry['change']}\nMonthly Share Purchase Ratio: {entry['mspr']}\n\n"
|
||||
seen_dicts.append(entry)
|
||||
|
||||
return (
|
||||
f"## {ticker} Insider Sentiment Data for {before} to {curr_date}:\n"
|
||||
+ result_str
|
||||
+ "The change field refers to the net buying/selling from all insiders' transactions. The mspr field refers to monthly share purchase ratio."
|
||||
)
|
||||
|
||||
|
||||
def get_finnhub_company_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
curr_date: Annotated[
|
||||
str,
|
||||
"current date you are trading at, yyyy-mm-dd",
|
||||
],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
):
|
||||
"""
|
||||
Retrieve insider transcaction information about a company (retrieved from public SEC information) for the past 15 days
|
||||
Args:
|
||||
ticker (str): ticker symbol of the company
|
||||
curr_date (str): current date you are trading at, yyyy-mm-dd
|
||||
Returns:
|
||||
str: a report of the company's insider transaction/trading informtaion in the past 15 days
|
||||
"""
|
||||
|
||||
date_obj = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = date_obj - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
data = get_data_in_range(ticker, before, curr_date, "insider_trans", DATA_DIR)
|
||||
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
|
||||
result_str = ""
|
||||
|
||||
seen_dicts = []
|
||||
for date, senti_list in data.items():
|
||||
for entry in senti_list:
|
||||
if entry not in seen_dicts:
|
||||
result_str += f"### Filing Date: {entry['filingDate']}, {entry['name']}:\nChange:{entry['change']}\nShares: {entry['share']}\nTransaction Price: {entry['transactionPrice']}\nTransaction Code: {entry['transactionCode']}\n\n"
|
||||
seen_dicts.append(entry)
|
||||
|
||||
return (
|
||||
f"## {ticker} insider transactions from {before} to {curr_date}:\n"
|
||||
+ result_str
|
||||
+ "The change field reflects the variation in share count—here a negative number indicates a reduction in holdings—while share specifies the total number of shares involved. The transactionPrice denotes the per-share price at which the trade was executed, and transactionDate marks when the transaction occurred. The name field identifies the insider making the trade, and transactionCode (e.g., S for sale) clarifies the nature of the transaction. FilingDate records when the transaction was officially reported, and the unique id links to the specific SEC filing, as indicated by the source. Additionally, the symbol ties the transaction to a particular company, isDerivative flags whether the trade involves derivative securities, and currency notes the currency context of the transaction."
|
||||
)
|
||||
|
||||
|
||||
def get_simfin_balance_sheet(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual / quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
data_path = os.path.join(
|
||||
DATA_DIR,
|
||||
"fundamental_data",
|
||||
"simfin_data_all",
|
||||
"balance_sheet",
|
||||
"companies",
|
||||
"us",
|
||||
f"us-balance-{freq}.csv",
|
||||
)
|
||||
df = pd.read_csv(data_path, sep=";")
|
||||
|
||||
# Convert date strings to datetime objects and remove any time components
|
||||
df["Report Date"] = pd.to_datetime(df["Report Date"], utc=True).dt.normalize()
|
||||
df["Publish Date"] = pd.to_datetime(df["Publish Date"], utc=True).dt.normalize()
|
||||
|
||||
# Convert the current date to datetime and normalize
|
||||
curr_date_dt = pd.to_datetime(curr_date, utc=True).normalize()
|
||||
|
||||
# Filter the DataFrame for the given ticker and for reports that were published on or before the current date
|
||||
filtered_df = df[(df["Ticker"] == ticker) & (df["Publish Date"] <= curr_date_dt)]
|
||||
|
||||
# Check if there are any available reports; if not, return a notification
|
||||
if filtered_df.empty:
|
||||
print("No balance sheet available before the given current date.")
|
||||
return ""
|
||||
|
||||
# Get the most recent balance sheet by selecting the row with the latest Publish Date
|
||||
latest_balance_sheet = filtered_df.loc[filtered_df["Publish Date"].idxmax()]
|
||||
|
||||
# drop the SimFinID column
|
||||
latest_balance_sheet = latest_balance_sheet.drop("SimFinId")
|
||||
|
||||
return (
|
||||
f"## {freq} balance sheet for {ticker} released on {str(latest_balance_sheet['Publish Date'])[0:10]}: \n"
|
||||
+ str(latest_balance_sheet)
|
||||
+ "\n\nThis includes metadata like reporting dates and currency, share details, and a breakdown of assets, liabilities, and equity. Assets are grouped as current (liquid items like cash and receivables) and noncurrent (long-term investments and property). Liabilities are split between short-term obligations and long-term debts, while equity reflects shareholder funds such as paid-in capital and retained earnings. Together, these components ensure that total assets equal the sum of liabilities and equity."
|
||||
)
|
||||
|
||||
|
||||
def get_simfin_cashflow(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual / quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
data_path = os.path.join(
|
||||
DATA_DIR,
|
||||
"fundamental_data",
|
||||
"simfin_data_all",
|
||||
"cash_flow",
|
||||
"companies",
|
||||
"us",
|
||||
f"us-cashflow-{freq}.csv",
|
||||
)
|
||||
df = pd.read_csv(data_path, sep=";")
|
||||
|
||||
# Convert date strings to datetime objects and remove any time components
|
||||
df["Report Date"] = pd.to_datetime(df["Report Date"], utc=True).dt.normalize()
|
||||
df["Publish Date"] = pd.to_datetime(df["Publish Date"], utc=True).dt.normalize()
|
||||
|
||||
# Convert the current date to datetime and normalize
|
||||
curr_date_dt = pd.to_datetime(curr_date, utc=True).normalize()
|
||||
|
||||
# Filter the DataFrame for the given ticker and for reports that were published on or before the current date
|
||||
filtered_df = df[(df["Ticker"] == ticker) & (df["Publish Date"] <= curr_date_dt)]
|
||||
|
||||
# Check if there are any available reports; if not, return a notification
|
||||
if filtered_df.empty:
|
||||
print("No cash flow statement available before the given current date.")
|
||||
return ""
|
||||
|
||||
# Get the most recent cash flow statement by selecting the row with the latest Publish Date
|
||||
latest_cash_flow = filtered_df.loc[filtered_df["Publish Date"].idxmax()]
|
||||
|
||||
# drop the SimFinID column
|
||||
latest_cash_flow = latest_cash_flow.drop("SimFinId")
|
||||
|
||||
return (
|
||||
f"## {freq} cash flow statement for {ticker} released on {str(latest_cash_flow['Publish Date'])[0:10]}: \n"
|
||||
+ str(latest_cash_flow)
|
||||
+ "\n\nThis includes metadata like reporting dates and currency, share details, and a breakdown of cash movements. Operating activities show cash generated from core business operations, including net income adjustments for non-cash items and working capital changes. Investing activities cover asset acquisitions/disposals and investments. Financing activities include debt transactions, equity issuances/repurchases, and dividend payments. The net change in cash represents the overall increase or decrease in the company's cash position during the reporting period."
|
||||
)
|
||||
|
||||
|
||||
def get_simfin_income_statements(
|
||||
ticker: Annotated[str, "ticker symbol"],
|
||||
freq: Annotated[
|
||||
str,
|
||||
"reporting frequency of the company's financial history: annual / quarterly",
|
||||
],
|
||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||
):
|
||||
data_path = os.path.join(
|
||||
DATA_DIR,
|
||||
"fundamental_data",
|
||||
"simfin_data_all",
|
||||
"income_statements",
|
||||
"companies",
|
||||
"us",
|
||||
f"us-income-{freq}.csv",
|
||||
)
|
||||
df = pd.read_csv(data_path, sep=";")
|
||||
|
||||
# Convert date strings to datetime objects and remove any time components
|
||||
df["Report Date"] = pd.to_datetime(df["Report Date"], utc=True).dt.normalize()
|
||||
df["Publish Date"] = pd.to_datetime(df["Publish Date"], utc=True).dt.normalize()
|
||||
|
||||
# Convert the current date to datetime and normalize
|
||||
curr_date_dt = pd.to_datetime(curr_date, utc=True).normalize()
|
||||
|
||||
# Filter the DataFrame for the given ticker and for reports that were published on or before the current date
|
||||
filtered_df = df[(df["Ticker"] == ticker) & (df["Publish Date"] <= curr_date_dt)]
|
||||
|
||||
# Check if there are any available reports; if not, return a notification
|
||||
if filtered_df.empty:
|
||||
print("No income statement available before the given current date.")
|
||||
return ""
|
||||
|
||||
# Get the most recent income statement by selecting the row with the latest Publish Date
|
||||
latest_income = filtered_df.loc[filtered_df["Publish Date"].idxmax()]
|
||||
|
||||
# drop the SimFinID column
|
||||
latest_income = latest_income.drop("SimFinId")
|
||||
|
||||
return (
|
||||
f"## {freq} income statement for {ticker} released on {str(latest_income['Publish Date'])[0:10]}: \n"
|
||||
+ str(latest_income)
|
||||
+ "\n\nThis includes metadata like reporting dates and currency, share details, and a comprehensive breakdown of the company's financial performance. Starting with Revenue, it shows Cost of Revenue and resulting Gross Profit. Operating Expenses are detailed, including SG&A, R&D, and Depreciation. The statement then shows Operating Income, followed by non-operating items and Interest Expense, leading to Pretax Income. After accounting for Income Tax and any Extraordinary items, it concludes with Net Income, representing the company's bottom-line profit or loss for the period."
|
||||
)
|
||||
|
||||
|
||||
def get_google_news(
|
||||
query: Annotated[str, "Query to search with"],
|
||||
curr_date: Annotated[str, "Curr date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
) -> str:
|
||||
query = query.replace(" ", "+")
|
||||
|
||||
start_date = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = start_date - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
news_results = getNewsData(query, before, curr_date)
|
||||
|
||||
news_str = ""
|
||||
|
||||
for news in news_results:
|
||||
news_str += (
|
||||
f"### {news['title']} (source: {news['source']}) \n\n{news['snippet']}\n\n"
|
||||
)
|
||||
|
||||
if len(news_results) == 0:
|
||||
return ""
|
||||
|
||||
return f"## {query} Google News, from {before} to {curr_date}:\n\n{news_str}"
|
||||
|
||||
|
||||
def get_reddit_global_news(
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
max_limit_per_day: Annotated[int, "Maximum number of news per day"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the latest top reddit news
|
||||
Args:
|
||||
start_date: Start date in yyyy-mm-dd format
|
||||
end_date: End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the latest news articles posts on reddit and meta information in these columns: "created_utc", "id", "title", "selftext", "score", "num_comments", "url"
|
||||
"""
|
||||
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
before = start_date - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
posts = []
|
||||
# iterate from start_date to end_date
|
||||
curr_date = datetime.strptime(before, "%Y-%m-%d")
|
||||
|
||||
total_iterations = (start_date - curr_date).days + 1
|
||||
pbar = tqdm(desc=f"Getting Global News on {start_date}", total=total_iterations)
|
||||
|
||||
while curr_date <= start_date:
|
||||
curr_date_str = curr_date.strftime("%Y-%m-%d")
|
||||
fetch_result = fetch_top_from_category(
|
||||
"global_news",
|
||||
curr_date_str,
|
||||
max_limit_per_day,
|
||||
data_path=os.path.join(DATA_DIR, "reddit_data"),
|
||||
)
|
||||
posts.extend(fetch_result)
|
||||
curr_date += relativedelta(days=1)
|
||||
pbar.update(1)
|
||||
|
||||
pbar.close()
|
||||
|
||||
if len(posts) == 0:
|
||||
return ""
|
||||
|
||||
news_str = ""
|
||||
for post in posts:
|
||||
if post["content"] == "":
|
||||
news_str += f"### {post['title']}\n\n"
|
||||
else:
|
||||
news_str += f"### {post['title']}\n\n{post['content']}\n\n"
|
||||
|
||||
return f"## Global News Reddit, from {before} to {curr_date}:\n{news_str}"
|
||||
|
||||
|
||||
def get_reddit_company_news(
|
||||
ticker: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
max_limit_per_day: Annotated[int, "Maximum number of news per day"],
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the latest top reddit news
|
||||
Args:
|
||||
ticker: ticker symbol of the company
|
||||
start_date: Start date in yyyy-mm-dd format
|
||||
end_date: End date in yyyy-mm-dd format
|
||||
Returns:
|
||||
str: A formatted dataframe containing the latest news articles posts on reddit and meta information in these columns: "created_utc", "id", "title", "selftext", "score", "num_comments", "url"
|
||||
"""
|
||||
|
||||
start_date = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
before = start_date - relativedelta(days=look_back_days)
|
||||
before = before.strftime("%Y-%m-%d")
|
||||
|
||||
posts = []
|
||||
# iterate from start_date to end_date
|
||||
curr_date = datetime.strptime(before, "%Y-%m-%d")
|
||||
|
||||
total_iterations = (start_date - curr_date).days + 1
|
||||
pbar = tqdm(
|
||||
desc=f"Getting Company News for {ticker} on {start_date}",
|
||||
total=total_iterations,
|
||||
)
|
||||
|
||||
while curr_date <= start_date:
|
||||
curr_date_str = curr_date.strftime("%Y-%m-%d")
|
||||
fetch_result = fetch_top_from_category(
|
||||
"company_news",
|
||||
curr_date_str,
|
||||
max_limit_per_day,
|
||||
ticker,
|
||||
data_path=os.path.join(DATA_DIR, "reddit_data"),
|
||||
)
|
||||
posts.extend(fetch_result)
|
||||
curr_date += relativedelta(days=1)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
pbar.close()
|
||||
|
||||
if len(posts) == 0:
|
||||
return ""
|
||||
|
||||
news_str = ""
|
||||
for post in posts:
|
||||
if post["content"] == "":
|
||||
news_str += f"### {post['title']}\n\n"
|
||||
else:
|
||||
news_str += f"### {post['title']}\n\n{post['content']}\n\n"
|
||||
|
||||
return f"##{ticker} News Reddit, from {before} to {curr_date}:\n\n{news_str}"
|
||||
|
||||
|
||||
def get_stock_stats_indicators_window(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
online: Annotated[bool, "to fetch data online or offline"],
|
||||
) -> str:
|
||||
|
||||
best_ind_params = {
|
||||
# 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."
|
||||
),
|
||||
"close_200_sma": (
|
||||
"200 SMA: A long-term trend benchmark. "
|
||||
"Usage: Confirm overall market trend and identify golden/death cross setups. "
|
||||
"Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries."
|
||||
),
|
||||
"close_10_ema": (
|
||||
"10 EMA: A responsive short-term average. "
|
||||
"Usage: Capture quick shifts in momentum and potential entry points. "
|
||||
"Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals."
|
||||
),
|
||||
# MACD Related
|
||||
"macd": (
|
||||
"MACD: Computes momentum via differences of EMAs. "
|
||||
"Usage: Look for crossovers and divergence as signals of trend changes. "
|
||||
"Tips: Confirm with other indicators in low-volatility or sideways markets."
|
||||
),
|
||||
"macds": (
|
||||
"MACD Signal: An EMA smoothing of the MACD line. "
|
||||
"Usage: Use crossovers with the MACD line to trigger trades. "
|
||||
"Tips: Should be part of a broader strategy to avoid false positives."
|
||||
),
|
||||
"macdh": (
|
||||
"MACD Histogram: Shows the gap between the MACD line and its signal. "
|
||||
"Usage: Visualize momentum strength and spot divergence early. "
|
||||
"Tips: Can be volatile; complement with additional filters in fast-moving markets."
|
||||
),
|
||||
# Momentum Indicators
|
||||
"rsi": (
|
||||
"RSI: Measures momentum to flag overbought/oversold conditions. "
|
||||
"Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. "
|
||||
"Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis."
|
||||
),
|
||||
# Volatility Indicators
|
||||
"boll": (
|
||||
"Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
|
||||
"Usage: Acts as a dynamic benchmark for price movement. "
|
||||
"Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals."
|
||||
),
|
||||
"boll_ub": (
|
||||
"Bollinger Upper Band: Typically 2 standard deviations above the middle line. "
|
||||
"Usage: Signals potential overbought conditions and breakout zones. "
|
||||
"Tips: Confirm signals with other tools; prices may ride the band in strong trends."
|
||||
),
|
||||
"boll_lb": (
|
||||
"Bollinger Lower Band: Typically 2 standard deviations below the middle line. "
|
||||
"Usage: Indicates potential oversold conditions. "
|
||||
"Tips: Use additional analysis to avoid false reversal signals."
|
||||
),
|
||||
"atr": (
|
||||
"ATR: Averages true range to measure volatility. "
|
||||
"Usage: Set stop-loss levels and adjust position sizes based on current market volatility. "
|
||||
"Tips: It's a reactive measure, so use it as part of a broader risk management strategy."
|
||||
),
|
||||
# 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."
|
||||
),
|
||||
"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 = curr_date
|
||||
curr_date = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = curr_date - relativedelta(days=look_back_days)
|
||||
|
||||
if not online:
|
||||
# read from YFin data
|
||||
data = pd.read_csv(
|
||||
os.path.join(
|
||||
DATA_DIR,
|
||||
f"market_data/price_data/{symbol}-YFin-data-2015-01-01-2025-03-25.csv",
|
||||
)
|
||||
)
|
||||
data["Date"] = pd.to_datetime(data["Date"], utc=True)
|
||||
dates_in_df = data["Date"].astype(str).str[:10]
|
||||
|
||||
ind_string = ""
|
||||
while curr_date >= before:
|
||||
# only do the trading dates
|
||||
if curr_date.strftime("%Y-%m-%d") in dates_in_df.values:
|
||||
indicator_value = get_stockstats_indicator(
|
||||
symbol, indicator, curr_date.strftime("%Y-%m-%d"), online
|
||||
)
|
||||
|
||||
ind_string += f"{curr_date.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
||||
|
||||
curr_date = curr_date - relativedelta(days=1)
|
||||
else:
|
||||
# online gathering
|
||||
ind_string = ""
|
||||
while curr_date >= before:
|
||||
indicator_value = get_stockstats_indicator(
|
||||
symbol, indicator, curr_date.strftime("%Y-%m-%d"), online
|
||||
)
|
||||
|
||||
ind_string += f"{curr_date.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
||||
|
||||
curr_date = curr_date - relativedelta(days=1)
|
||||
|
||||
result_str = (
|
||||
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
|
||||
+ ind_string
|
||||
+ "\n\n"
|
||||
+ best_ind_params.get(indicator, "No description available.")
|
||||
)
|
||||
|
||||
return result_str
|
||||
|
||||
|
||||
def get_stockstats_indicator(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
online: Annotated[bool, "to fetch data online or offline"],
|
||||
) -> str:
|
||||
|
||||
curr_date = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
curr_date = curr_date.strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
indicator_value = StockstatsUtils.get_stock_stats(
|
||||
symbol,
|
||||
indicator,
|
||||
curr_date,
|
||||
os.path.join(DATA_DIR, "market_data", "price_data"),
|
||||
online=online,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error getting stockstats indicator data for indicator {indicator} on {curr_date}: {e}"
|
||||
)
|
||||
return ""
|
||||
|
||||
return str(indicator_value)
|
||||
|
||||
|
||||
def get_YFin_data_window(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
curr_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
) -> str:
|
||||
# calculate past days
|
||||
date_obj = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = date_obj - relativedelta(days=look_back_days)
|
||||
start_date = before.strftime("%Y-%m-%d")
|
||||
|
||||
# read in data
|
||||
data = pd.read_csv(
|
||||
os.path.join(
|
||||
DATA_DIR,
|
||||
f"market_data/price_data/{symbol}-YFin-data-2015-01-01-2025-03-25.csv",
|
||||
)
|
||||
)
|
||||
|
||||
# Extract just the date part for comparison
|
||||
data["DateOnly"] = data["Date"].str[:10]
|
||||
|
||||
# Filter data between the start and end dates (inclusive)
|
||||
filtered_data = data[
|
||||
(data["DateOnly"] >= start_date) & (data["DateOnly"] <= curr_date)
|
||||
]
|
||||
|
||||
# Drop the temporary column we created
|
||||
filtered_data = filtered_data.drop("DateOnly", axis=1)
|
||||
|
||||
# Set pandas display options to show the full DataFrame
|
||||
with pd.option_context(
|
||||
"display.max_rows", None, "display.max_columns", None, "display.width", None
|
||||
):
|
||||
df_string = filtered_data.to_string()
|
||||
|
||||
return (
|
||||
f"## Raw Market Data for {symbol} from {start_date} to {curr_date}:\n\n"
|
||||
+ df_string
|
||||
)
|
||||
|
||||
|
||||
def get_YFin_data_online(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
):
|
||||
|
||||
datetime.strptime(start_date, "%Y-%m-%d")
|
||||
datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
# Create ticker object
|
||||
ticker = yf.Ticker(symbol.upper())
|
||||
|
||||
# Fetch historical data for the specified date range
|
||||
data = ticker.history(start=start_date, end=end_date)
|
||||
|
||||
# Check if data is empty
|
||||
if data.empty:
|
||||
return (
|
||||
f"No data found for symbol '{symbol}' between {start_date} and {end_date}"
|
||||
)
|
||||
|
||||
# Remove timezone info from index for cleaner output
|
||||
if data.index.tz is not None:
|
||||
data.index = data.index.tz_localize(None)
|
||||
|
||||
# Round numerical values to 2 decimal places for cleaner display
|
||||
numeric_columns = ["Open", "High", "Low", "Close", "Adj Close"]
|
||||
for col in numeric_columns:
|
||||
if col in data.columns:
|
||||
data[col] = data[col].round(2)
|
||||
|
||||
# Convert DataFrame to CSV string
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Stock 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"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
|
||||
def get_YFin_data(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
) -> str:
|
||||
# read in data
|
||||
data = pd.read_csv(
|
||||
os.path.join(
|
||||
DATA_DIR,
|
||||
f"market_data/price_data/{symbol}-YFin-data-2015-01-01-2025-03-25.csv",
|
||||
)
|
||||
)
|
||||
|
||||
if end_date > "2025-03-25":
|
||||
raise Exception(
|
||||
f"Get_YFin_Data: {end_date} is outside of the data range of 2015-01-01 to 2025-03-25"
|
||||
)
|
||||
|
||||
# Extract just the date part for comparison
|
||||
data["DateOnly"] = data["Date"].str[:10]
|
||||
|
||||
# Filter data between the start and end dates (inclusive)
|
||||
filtered_data = data[
|
||||
(data["DateOnly"] >= start_date) & (data["DateOnly"] <= end_date)
|
||||
]
|
||||
|
||||
# Drop the temporary column we created
|
||||
filtered_data = filtered_data.drop("DateOnly", axis=1)
|
||||
|
||||
# remove the index from the dataframe
|
||||
filtered_data = filtered_data.reset_index(drop=True)
|
||||
|
||||
return filtered_data
|
||||
|
||||
|
||||
def get_stock_news_openai(ticker, curr_date):
|
||||
client = OpenAI()
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-4.1-mini",
|
||||
input=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": f"Can you search Social Media for {ticker} on TSLA from 7 days before {curr_date} to {curr_date}? Make sure you only get the data posted during that period.",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
text={"format": {"type": "text"}},
|
||||
reasoning={},
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search_preview",
|
||||
"user_location": {"type": "approximate"},
|
||||
"search_context_size": "low",
|
||||
}
|
||||
],
|
||||
temperature=1,
|
||||
max_output_tokens=4096,
|
||||
top_p=1,
|
||||
store=True,
|
||||
)
|
||||
|
||||
return response.output[1].content[0].text
|
||||
|
||||
|
||||
def get_global_news_openai(curr_date):
|
||||
client = OpenAI()
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-4.1-mini",
|
||||
input=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": f"Can you search global or macroeconomics news from 7 days before {curr_date} to {curr_date} that would be informative for trading purposes? Make sure you only get the data posted during that period.",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
text={"format": {"type": "text"}},
|
||||
reasoning={},
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search_preview",
|
||||
"user_location": {"type": "approximate"},
|
||||
"search_context_size": "low",
|
||||
}
|
||||
],
|
||||
temperature=1,
|
||||
max_output_tokens=4096,
|
||||
top_p=1,
|
||||
store=True,
|
||||
)
|
||||
|
||||
return response.output[1].content[0].text
|
||||
|
||||
|
||||
def get_fundamentals_openai(ticker, curr_date):
|
||||
client = OpenAI()
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-4.1-mini",
|
||||
input=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": f"Can you search Fundamental for discussions on {ticker} during of the month before {curr_date} to the month of {curr_date}. Make sure you only get the data posted during that period. List as a table, with PE/PS/Cash flow/ etc",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
text={"format": {"type": "text"}},
|
||||
reasoning={},
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search_preview",
|
||||
"user_location": {"type": "approximate"},
|
||||
"search_context_size": "low",
|
||||
}
|
||||
],
|
||||
temperature=1,
|
||||
max_output_tokens=4096,
|
||||
top_p=1,
|
||||
store=True,
|
||||
)
|
||||
|
||||
return response.output[1].content[0].text
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
import requests
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import contextmanager
|
||||
from typing import Annotated
|
||||
import os
|
||||
import re
|
||||
|
||||
ticker_to_company = {
|
||||
"AAPL": "Apple",
|
||||
"MSFT": "Microsoft",
|
||||
"GOOGL": "Google",
|
||||
"AMZN": "Amazon",
|
||||
"TSLA": "Tesla",
|
||||
"NVDA": "Nvidia",
|
||||
"TSM": "Taiwan Semiconductor Manufacturing Company OR TSMC",
|
||||
"JPM": "JPMorgan Chase OR JP Morgan",
|
||||
"JNJ": "Johnson & Johnson OR JNJ",
|
||||
"V": "Visa",
|
||||
"WMT": "Walmart",
|
||||
"META": "Meta OR Facebook",
|
||||
"AMD": "AMD",
|
||||
"INTC": "Intel",
|
||||
"QCOM": "Qualcomm",
|
||||
"BABA": "Alibaba",
|
||||
"ADBE": "Adobe",
|
||||
"NFLX": "Netflix",
|
||||
"CRM": "Salesforce",
|
||||
"PYPL": "PayPal",
|
||||
"PLTR": "Palantir",
|
||||
"MU": "Micron",
|
||||
"SQ": "Block OR Square",
|
||||
"ZM": "Zoom",
|
||||
"CSCO": "Cisco",
|
||||
"SHOP": "Shopify",
|
||||
"ORCL": "Oracle",
|
||||
"X": "Twitter OR X",
|
||||
"SPOT": "Spotify",
|
||||
"AVGO": "Broadcom",
|
||||
"ASML": "ASML ",
|
||||
"TWLO": "Twilio",
|
||||
"SNAP": "Snap Inc.",
|
||||
"TEAM": "Atlassian",
|
||||
"SQSP": "Squarespace",
|
||||
"UBER": "Uber",
|
||||
"ROKU": "Roku",
|
||||
"PINS": "Pinterest",
|
||||
}
|
||||
|
||||
|
||||
def fetch_top_from_category(
|
||||
category: Annotated[
|
||||
str, "Category to fetch top post from. Collection of subreddits."
|
||||
],
|
||||
date: Annotated[str, "Date to fetch top posts from."],
|
||||
max_limit: Annotated[int, "Maximum number of posts to fetch."],
|
||||
query: Annotated[str, "Optional query to search for in the subreddit."] = None,
|
||||
data_path: Annotated[
|
||||
str,
|
||||
"Path to the data folder. Default is 'reddit_data'.",
|
||||
] = "reddit_data",
|
||||
):
|
||||
base_path = data_path
|
||||
|
||||
all_content = []
|
||||
|
||||
if max_limit < len(os.listdir(os.path.join(base_path, category))):
|
||||
raise ValueError(
|
||||
"REDDIT FETCHING ERROR: max limit is less than the number of files in the category. Will not be able to fetch any posts"
|
||||
)
|
||||
|
||||
limit_per_subreddit = max_limit // len(
|
||||
os.listdir(os.path.join(base_path, category))
|
||||
)
|
||||
|
||||
for data_file in os.listdir(os.path.join(base_path, category)):
|
||||
# check if data_file is a .jsonl file
|
||||
if not data_file.endswith(".jsonl"):
|
||||
continue
|
||||
|
||||
all_content_curr_subreddit = []
|
||||
|
||||
with open(os.path.join(base_path, category, data_file), "rb") as f:
|
||||
for i, line in enumerate(f):
|
||||
# skip empty lines
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
parsed_line = json.loads(line)
|
||||
|
||||
# select only lines that are from the date
|
||||
post_date = datetime.utcfromtimestamp(
|
||||
parsed_line["created_utc"]
|
||||
).strftime("%Y-%m-%d")
|
||||
if post_date != date:
|
||||
continue
|
||||
|
||||
# if is company_news, check that the title or the content has the company's name (query) mentioned
|
||||
if "company" in category and query:
|
||||
search_terms = []
|
||||
if "OR" in ticker_to_company[query]:
|
||||
search_terms = ticker_to_company[query].split(" OR ")
|
||||
else:
|
||||
search_terms = [ticker_to_company[query]]
|
||||
|
||||
search_terms.append(query)
|
||||
|
||||
found = False
|
||||
for term in search_terms:
|
||||
if re.search(
|
||||
term, parsed_line["title"], re.IGNORECASE
|
||||
) or re.search(term, parsed_line["selftext"], re.IGNORECASE):
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
continue
|
||||
|
||||
post = {
|
||||
"title": parsed_line["title"],
|
||||
"content": parsed_line["selftext"],
|
||||
"url": parsed_line["url"],
|
||||
"upvotes": parsed_line["ups"],
|
||||
"posted_date": post_date,
|
||||
}
|
||||
|
||||
all_content_curr_subreddit.append(post)
|
||||
|
||||
# sort all_content_curr_subreddit by upvote_ratio in descending order
|
||||
all_content_curr_subreddit.sort(key=lambda x: x["upvotes"], reverse=True)
|
||||
|
||||
all_content.extend(all_content_curr_subreddit[:limit_per_subreddit])
|
||||
|
||||
return all_content
|
||||
|
|
@ -1,106 +1,10 @@
|
|||
import time
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
from yfinance.exceptions import YFRateLimitError
|
||||
from stockstats import wrap
|
||||
from typing import Annotated
|
||||
import os
|
||||
from .config import get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def yf_retry(func, max_retries=3, base_delay=2.0):
|
||||
"""Execute a yfinance call with exponential backoff on rate limits.
|
||||
|
||||
yfinance raises YFRateLimitError on HTTP 429 responses but does not
|
||||
retry them internally. This wrapper adds retry logic specifically
|
||||
for rate limits. Other exceptions propagate immediately.
|
||||
"""
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return func()
|
||||
except YFRateLimitError:
|
||||
if attempt < max_retries:
|
||||
delay = base_delay * (2 ** attempt)
|
||||
logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def _clean_dataframe(data: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Normalize a stock DataFrame for stockstats: parse dates, drop invalid rows, fill price gaps."""
|
||||
data["Date"] = pd.to_datetime(data["Date"], errors="coerce")
|
||||
data = data.dropna(subset=["Date"])
|
||||
|
||||
price_cols = [c for c in ["Open", "High", "Low", "Close", "Volume"] if c in data.columns]
|
||||
data[price_cols] = data[price_cols].apply(pd.to_numeric, errors="coerce")
|
||||
data = data.dropna(subset=["Close"])
|
||||
data[price_cols] = data[price_cols].ffill().bfill()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def load_ohlcv(symbol: str, curr_date: str) -> pd.DataFrame:
|
||||
"""Fetch OHLCV data with caching, filtered to prevent look-ahead bias.
|
||||
|
||||
Downloads 15 years of data up to today and caches per symbol. On
|
||||
subsequent calls the cache is reused. Rows after curr_date are
|
||||
filtered out so backtests never see future prices.
|
||||
"""
|
||||
config = get_config()
|
||||
curr_date_dt = pd.to_datetime(curr_date)
|
||||
|
||||
# Cache uses a fixed window (15y to today) so one file per symbol
|
||||
today_date = pd.Timestamp.today()
|
||||
start_date = today_date - pd.DateOffset(years=5)
|
||||
start_str = start_date.strftime("%Y-%m-%d")
|
||||
end_str = today_date.strftime("%Y-%m-%d")
|
||||
|
||||
os.makedirs(config["data_cache_dir"], exist_ok=True)
|
||||
data_file = os.path.join(
|
||||
config["data_cache_dir"],
|
||||
f"{symbol}-YFin-data-{start_str}-{end_str}.csv",
|
||||
)
|
||||
|
||||
if os.path.exists(data_file):
|
||||
data = pd.read_csv(data_file, on_bad_lines="skip")
|
||||
else:
|
||||
data = yf_retry(lambda: yf.download(
|
||||
symbol,
|
||||
start=start_str,
|
||||
end=end_str,
|
||||
multi_level_index=False,
|
||||
progress=False,
|
||||
auto_adjust=True,
|
||||
))
|
||||
data = data.reset_index()
|
||||
data.to_csv(data_file, index=False)
|
||||
|
||||
data = _clean_dataframe(data)
|
||||
|
||||
# Filter to curr_date to prevent look-ahead bias in backtesting
|
||||
data = data[data["Date"] <= curr_date_dt]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame:
|
||||
"""Drop financial statement columns (fiscal period timestamps) after curr_date.
|
||||
|
||||
yfinance financial statements use fiscal period end dates as columns.
|
||||
Columns after curr_date represent future data and are removed to
|
||||
prevent look-ahead bias.
|
||||
"""
|
||||
if not curr_date or data.empty:
|
||||
return data
|
||||
cutoff = pd.Timestamp(curr_date)
|
||||
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
|
||||
return data.loc[:, mask]
|
||||
|
||||
|
||||
class StockstatsUtils:
|
||||
@staticmethod
|
||||
|
|
@ -112,14 +16,69 @@ class StockstatsUtils:
|
|||
curr_date: Annotated[
|
||||
str, "curr date for retrieving stock price data, YYYY-mm-dd"
|
||||
],
|
||||
data_dir: Annotated[
|
||||
str,
|
||||
"directory where the stock data is stored.",
|
||||
],
|
||||
online: Annotated[
|
||||
bool,
|
||||
"whether to use online tools to fetch data or offline tools. If True, will use online tools.",
|
||||
] = False,
|
||||
):
|
||||
data = load_ohlcv(symbol, curr_date)
|
||||
df = wrap(data)
|
||||
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
||||
curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d")
|
||||
df = None
|
||||
data = None
|
||||
|
||||
if not online:
|
||||
try:
|
||||
data = pd.read_csv(
|
||||
os.path.join(
|
||||
data_dir,
|
||||
f"{symbol}-YFin-data-2015-01-01-2025-03-25.csv",
|
||||
)
|
||||
)
|
||||
df = wrap(data)
|
||||
except FileNotFoundError:
|
||||
raise Exception("Stockstats fail: Yahoo Finance data not fetched yet!")
|
||||
else:
|
||||
# Get today's date as YYYY-mm-dd to add to cache
|
||||
today_date = pd.Timestamp.today()
|
||||
curr_date = pd.to_datetime(curr_date)
|
||||
|
||||
end_date = today_date
|
||||
start_date = today_date - pd.DateOffset(years=15)
|
||||
start_date = start_date.strftime("%Y-%m-%d")
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Get config and ensure cache directory exists
|
||||
config = get_config()
|
||||
os.makedirs(config["data_cache_dir"], exist_ok=True)
|
||||
|
||||
data_file = os.path.join(
|
||||
config["data_cache_dir"],
|
||||
f"{symbol}-YFin-data-{start_date}-{end_date}.csv",
|
||||
)
|
||||
|
||||
if os.path.exists(data_file):
|
||||
data = pd.read_csv(data_file)
|
||||
data["Date"] = pd.to_datetime(data["Date"])
|
||||
else:
|
||||
data = yf.download(
|
||||
symbol,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
multi_level_index=False,
|
||||
progress=False,
|
||||
auto_adjust=True,
|
||||
)
|
||||
data = data.reset_index()
|
||||
data.to_csv(data_file, index=False)
|
||||
|
||||
df = wrap(data)
|
||||
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
||||
curr_date = curr_date.strftime("%Y-%m-%d")
|
||||
|
||||
df[indicator] # trigger stockstats to calculate the indicator
|
||||
matching_rows = df[df["Date"].str.startswith(curr_date_str)]
|
||||
matching_rows = df[df["Date"].str.startswith(curr_date)]
|
||||
|
||||
if not matching_rows.empty:
|
||||
indicator_value = matching_rows[indicator].values[0]
|
||||
|
|
|
|||
|
|
@ -1,422 +0,0 @@
|
|||
from typing import Annotated
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
import os
|
||||
from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date
|
||||
|
||||
def get_YFin_data_online(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||
):
|
||||
|
||||
datetime.strptime(start_date, "%Y-%m-%d")
|
||||
datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
# Create ticker object
|
||||
ticker = yf.Ticker(symbol.upper())
|
||||
|
||||
# Fetch historical data for the specified date range
|
||||
data = yf_retry(lambda: ticker.history(start=start_date, end=end_date))
|
||||
|
||||
# Check if data is empty
|
||||
if data.empty:
|
||||
return (
|
||||
f"No data found for symbol '{symbol}' between {start_date} and {end_date}"
|
||||
)
|
||||
|
||||
# Remove timezone info from index for cleaner output
|
||||
if data.index.tz is not None:
|
||||
data.index = data.index.tz_localize(None)
|
||||
|
||||
# Round numerical values to 2 decimal places for cleaner display
|
||||
numeric_columns = ["Open", "High", "Low", "Close", "Adj Close"]
|
||||
for col in numeric_columns:
|
||||
if col in data.columns:
|
||||
data[col] = data[col].round(2)
|
||||
|
||||
# Convert DataFrame to CSV string
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Stock 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"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
def get_stock_stats_indicators_window(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
look_back_days: Annotated[int, "how many days to look back"],
|
||||
) -> str:
|
||||
|
||||
best_ind_params = {
|
||||
# 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."
|
||||
),
|
||||
"close_200_sma": (
|
||||
"200 SMA: A long-term trend benchmark. "
|
||||
"Usage: Confirm overall market trend and identify golden/death cross setups. "
|
||||
"Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries."
|
||||
),
|
||||
"close_10_ema": (
|
||||
"10 EMA: A responsive short-term average. "
|
||||
"Usage: Capture quick shifts in momentum and potential entry points. "
|
||||
"Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals."
|
||||
),
|
||||
# MACD Related
|
||||
"macd": (
|
||||
"MACD: Computes momentum via differences of EMAs. "
|
||||
"Usage: Look for crossovers and divergence as signals of trend changes. "
|
||||
"Tips: Confirm with other indicators in low-volatility or sideways markets."
|
||||
),
|
||||
"macds": (
|
||||
"MACD Signal: An EMA smoothing of the MACD line. "
|
||||
"Usage: Use crossovers with the MACD line to trigger trades. "
|
||||
"Tips: Should be part of a broader strategy to avoid false positives."
|
||||
),
|
||||
"macdh": (
|
||||
"MACD Histogram: Shows the gap between the MACD line and its signal. "
|
||||
"Usage: Visualize momentum strength and spot divergence early. "
|
||||
"Tips: Can be volatile; complement with additional filters in fast-moving markets."
|
||||
),
|
||||
# Momentum Indicators
|
||||
"rsi": (
|
||||
"RSI: Measures momentum to flag overbought/oversold conditions. "
|
||||
"Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. "
|
||||
"Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis."
|
||||
),
|
||||
# Volatility Indicators
|
||||
"boll": (
|
||||
"Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
|
||||
"Usage: Acts as a dynamic benchmark for price movement. "
|
||||
"Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals."
|
||||
),
|
||||
"boll_ub": (
|
||||
"Bollinger Upper Band: Typically 2 standard deviations above the middle line. "
|
||||
"Usage: Signals potential overbought conditions and breakout zones. "
|
||||
"Tips: Confirm signals with other tools; prices may ride the band in strong trends."
|
||||
),
|
||||
"boll_lb": (
|
||||
"Bollinger Lower Band: Typically 2 standard deviations below the middle line. "
|
||||
"Usage: Indicates potential oversold conditions. "
|
||||
"Tips: Use additional analysis to avoid false reversal signals."
|
||||
),
|
||||
"atr": (
|
||||
"ATR: Averages true range to measure volatility. "
|
||||
"Usage: Set stop-loss levels and adjust position sizes based on current market volatility. "
|
||||
"Tips: It's a reactive measure, so use it as part of a broader risk management strategy."
|
||||
),
|
||||
# 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."
|
||||
),
|
||||
"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 = curr_date
|
||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
before = curr_date_dt - relativedelta(days=look_back_days)
|
||||
|
||||
# Optimized: Get stock data once and calculate indicators for all dates
|
||||
try:
|
||||
indicator_data = _get_stock_stats_bulk(symbol, indicator, curr_date)
|
||||
|
||||
# Generate the date range we need
|
||||
current_dt = curr_date_dt
|
||||
date_values = []
|
||||
|
||||
while current_dt >= before:
|
||||
date_str = current_dt.strftime('%Y-%m-%d')
|
||||
|
||||
# Look up the indicator value for this date
|
||||
if date_str in indicator_data:
|
||||
indicator_value = indicator_data[date_str]
|
||||
else:
|
||||
indicator_value = "N/A: Not a trading day (weekend or holiday)"
|
||||
|
||||
date_values.append((date_str, indicator_value))
|
||||
current_dt = current_dt - relativedelta(days=1)
|
||||
|
||||
# Build the result string
|
||||
ind_string = ""
|
||||
for date_str, value in date_values:
|
||||
ind_string += f"{date_str}: {value}\n"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting bulk stockstats data: {e}")
|
||||
# Fallback to original implementation if bulk method fails
|
||||
ind_string = ""
|
||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
while curr_date_dt >= before:
|
||||
indicator_value = get_stockstats_indicator(
|
||||
symbol, indicator, curr_date_dt.strftime("%Y-%m-%d")
|
||||
)
|
||||
ind_string += f"{curr_date_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
||||
curr_date_dt = curr_date_dt - relativedelta(days=1)
|
||||
|
||||
result_str = (
|
||||
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
|
||||
+ ind_string
|
||||
+ "\n\n"
|
||||
+ best_ind_params.get(indicator, "No description available.")
|
||||
)
|
||||
|
||||
return result_str
|
||||
|
||||
|
||||
def _get_stock_stats_bulk(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to calculate"],
|
||||
curr_date: Annotated[str, "current date for reference"]
|
||||
) -> dict:
|
||||
"""
|
||||
Optimized bulk calculation of stock stats indicators.
|
||||
Fetches data once and calculates indicator for all available dates.
|
||||
Returns dict mapping date strings to indicator values.
|
||||
"""
|
||||
from stockstats import wrap
|
||||
|
||||
data = load_ohlcv(symbol, curr_date)
|
||||
df = wrap(data)
|
||||
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
||||
|
||||
# Calculate the indicator for all rows at once
|
||||
df[indicator] # This triggers stockstats to calculate the indicator
|
||||
|
||||
# Create a dictionary mapping date strings to indicator values
|
||||
result_dict = {}
|
||||
for _, row in df.iterrows():
|
||||
date_str = row["Date"]
|
||||
indicator_value = row[indicator]
|
||||
|
||||
# Handle NaN/None values
|
||||
if pd.isna(indicator_value):
|
||||
result_dict[date_str] = "N/A"
|
||||
else:
|
||||
result_dict[date_str] = str(indicator_value)
|
||||
|
||||
return result_dict
|
||||
|
||||
|
||||
def get_stockstats_indicator(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||
curr_date: Annotated[
|
||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||
],
|
||||
) -> str:
|
||||
|
||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
curr_date = curr_date_dt.strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
indicator_value = StockstatsUtils.get_stock_stats(
|
||||
symbol,
|
||||
indicator,
|
||||
curr_date,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error getting stockstats indicator data for indicator {indicator} on {curr_date}: {e}"
|
||||
)
|
||||
return ""
|
||||
|
||||
return str(indicator_value)
|
||||
|
||||
|
||||
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."""
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
info = yf_retry(lambda: ticker_obj.info)
|
||||
|
||||
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")),
|
||||
]
|
||||
|
||||
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"
|
||||
|
||||
return header + "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
return f"Error retrieving fundamentals for {ticker}: {str(e)}"
|
||||
|
||||
|
||||
def get_balance_sheet(
|
||||
ticker: Annotated[str, "ticker symbol of the company"],
|
||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get balance sheet data from yfinance."""
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_balance_sheet)
|
||||
else:
|
||||
data = yf_retry(lambda: ticker_obj.balance_sheet)
|
||||
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No balance sheet data found for symbol '{ticker}'"
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Balance Sheet data for {ticker.upper()} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
except Exception as e:
|
||||
return f"Error retrieving balance sheet for {ticker}: {str(e)}"
|
||||
|
||||
|
||||
def get_cashflow(
|
||||
ticker: Annotated[str, "ticker symbol of the company"],
|
||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get cash flow data from yfinance."""
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_cashflow)
|
||||
else:
|
||||
data = yf_retry(lambda: ticker_obj.cashflow)
|
||||
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No cash flow data found for symbol '{ticker}'"
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Cash Flow data for {ticker.upper()} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
except Exception as e:
|
||||
return f"Error retrieving cash flow for {ticker}: {str(e)}"
|
||||
|
||||
|
||||
def get_income_statement(
|
||||
ticker: Annotated[str, "ticker symbol of the company"],
|
||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get income statement data from yfinance."""
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_income_stmt)
|
||||
else:
|
||||
data = yf_retry(lambda: ticker_obj.income_stmt)
|
||||
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No income statement data found for symbol '{ticker}'"
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Income Statement data for {ticker.upper()} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
except Exception as e:
|
||||
return f"Error retrieving income statement for {ticker}: {str(e)}"
|
||||
|
||||
|
||||
def get_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol of the company"]
|
||||
):
|
||||
"""Get insider transactions data from yfinance."""
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
data = yf_retry(lambda: ticker_obj.insider_transactions)
|
||||
|
||||
if data is None or data.empty:
|
||||
return f"No insider transactions data found for symbol '{ticker}'"
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Insider Transactions data for {ticker.upper()}\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
except Exception as e:
|
||||
return f"Error retrieving insider transactions for {ticker}: {str(e)}"
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
# gets data/stats
|
||||
|
||||
import yfinance as yf
|
||||
from typing import Annotated, Callable, Any, Optional
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
from functools import wraps
|
||||
|
||||
from .utils import save_output, SavePathType, decorate_all_methods
|
||||
|
||||
|
||||
def init_ticker(func: Callable) -> Callable:
|
||||
"""Decorator to initialize yf.Ticker and pass it to the function."""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(symbol: Annotated[str, "ticker symbol"], *args, **kwargs) -> Any:
|
||||
ticker = yf.Ticker(symbol)
|
||||
return func(ticker, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@decorate_all_methods(init_ticker)
|
||||
class YFinanceUtils:
|
||||
|
||||
def get_stock_data(
|
||||
symbol: Annotated[str, "ticker symbol"],
|
||||
start_date: Annotated[
|
||||
str, "start date for retrieving stock price data, YYYY-mm-dd"
|
||||
],
|
||||
end_date: Annotated[
|
||||
str, "end date for retrieving stock price data, YYYY-mm-dd"
|
||||
],
|
||||
save_path: SavePathType = None,
|
||||
) -> DataFrame:
|
||||
"""retrieve stock price data for designated ticker symbol"""
|
||||
ticker = symbol
|
||||
# add one day to the end_date so that the data range is inclusive
|
||||
end_date = pd.to_datetime(end_date) + pd.DateOffset(days=1)
|
||||
end_date = end_date.strftime("%Y-%m-%d")
|
||||
stock_data = ticker.history(start=start_date, end=end_date)
|
||||
# save_output(stock_data, f"Stock data for {ticker.ticker}", save_path)
|
||||
return stock_data
|
||||
|
||||
def get_stock_info(
|
||||
symbol: Annotated[str, "ticker symbol"],
|
||||
) -> dict:
|
||||
"""Fetches and returns latest stock information."""
|
||||
ticker = symbol
|
||||
stock_info = ticker.info
|
||||
return stock_info
|
||||
|
||||
def get_company_info(
|
||||
symbol: Annotated[str, "ticker symbol"],
|
||||
save_path: Optional[str] = None,
|
||||
) -> DataFrame:
|
||||
"""Fetches and returns company information as a DataFrame."""
|
||||
ticker = symbol
|
||||
info = ticker.info
|
||||
company_info = {
|
||||
"Company Name": info.get("shortName", "N/A"),
|
||||
"Industry": info.get("industry", "N/A"),
|
||||
"Sector": info.get("sector", "N/A"),
|
||||
"Country": info.get("country", "N/A"),
|
||||
"Website": info.get("website", "N/A"),
|
||||
}
|
||||
company_info_df = DataFrame([company_info])
|
||||
if save_path:
|
||||
company_info_df.to_csv(save_path)
|
||||
print(f"Company info for {ticker.ticker} saved to {save_path}")
|
||||
return company_info_df
|
||||
|
||||
def get_stock_dividends(
|
||||
symbol: Annotated[str, "ticker symbol"],
|
||||
save_path: Optional[str] = None,
|
||||
) -> DataFrame:
|
||||
"""Fetches and returns the latest dividends data as a DataFrame."""
|
||||
ticker = symbol
|
||||
dividends = ticker.dividends
|
||||
if save_path:
|
||||
dividends.to_csv(save_path)
|
||||
print(f"Dividends for {ticker.ticker} saved to {save_path}")
|
||||
return dividends
|
||||
|
||||
def get_income_stmt(symbol: Annotated[str, "ticker symbol"]) -> DataFrame:
|
||||
"""Fetches and returns the latest income statement of the company as a DataFrame."""
|
||||
ticker = symbol
|
||||
income_stmt = ticker.financials
|
||||
return income_stmt
|
||||
|
||||
def get_balance_sheet(symbol: Annotated[str, "ticker symbol"]) -> DataFrame:
|
||||
"""Fetches and returns the latest balance sheet of the company as a DataFrame."""
|
||||
ticker = symbol
|
||||
balance_sheet = ticker.balance_sheet
|
||||
return balance_sheet
|
||||
|
||||
def get_cash_flow(symbol: Annotated[str, "ticker symbol"]) -> DataFrame:
|
||||
"""Fetches and returns the latest cash flow statement of the company as a DataFrame."""
|
||||
ticker = symbol
|
||||
cash_flow = ticker.cashflow
|
||||
return cash_flow
|
||||
|
||||
def get_analyst_recommendations(symbol: Annotated[str, "ticker symbol"]) -> tuple:
|
||||
"""Fetches the latest analyst recommendations and returns the most common recommendation and its count."""
|
||||
ticker = symbol
|
||||
recommendations = ticker.recommendations
|
||||
if recommendations.empty:
|
||||
return None, 0 # No recommendations available
|
||||
|
||||
# Assuming 'period' column exists and needs to be excluded
|
||||
row_0 = recommendations.iloc[0, 1:] # Exclude 'period' column if necessary
|
||||
|
||||
# Find the maximum voting result
|
||||
max_votes = row_0.max()
|
||||
majority_voting_result = row_0[row_0 == max_votes].index.tolist()
|
||||
|
||||
return majority_voting_result[0], max_votes
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
"""yfinance-based news data fetching functions."""
|
||||
|
||||
import yfinance as yf
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from .stockstats_utils import yf_retry
|
||||
|
||||
|
||||
def _extract_article_data(article: dict) -> dict:
|
||||
"""Extract article data from yfinance news format (handles nested 'content' structure)."""
|
||||
# Handle nested content structure
|
||||
if "content" in article:
|
||||
content = article["content"]
|
||||
title = content.get("title", "No title")
|
||||
summary = content.get("summary", "")
|
||||
provider = content.get("provider", {})
|
||||
publisher = provider.get("displayName", "Unknown")
|
||||
|
||||
# Get URL from canonicalUrl or clickThroughUrl
|
||||
url_obj = content.get("canonicalUrl") or content.get("clickThroughUrl") or {}
|
||||
link = url_obj.get("url", "")
|
||||
|
||||
# Get publish date
|
||||
pub_date_str = content.get("pubDate", "")
|
||||
pub_date = None
|
||||
if pub_date_str:
|
||||
try:
|
||||
pub_date = datetime.fromisoformat(pub_date_str.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"summary": summary,
|
||||
"publisher": publisher,
|
||||
"link": link,
|
||||
"pub_date": pub_date,
|
||||
}
|
||||
else:
|
||||
# Fallback for flat structure
|
||||
return {
|
||||
"title": article.get("title", "No title"),
|
||||
"summary": article.get("summary", ""),
|
||||
"publisher": article.get("publisher", "Unknown"),
|
||||
"link": article.get("link", ""),
|
||||
"pub_date": None,
|
||||
}
|
||||
|
||||
|
||||
def get_news_yfinance(
|
||||
ticker: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve news for a specific stock ticker using yfinance.
|
||||
|
||||
Args:
|
||||
ticker: Stock ticker symbol (e.g., "AAPL")
|
||||
start_date: Start date in yyyy-mm-dd format
|
||||
end_date: End date in yyyy-mm-dd format
|
||||
|
||||
Returns:
|
||||
Formatted string containing news articles
|
||||
"""
|
||||
try:
|
||||
stock = yf.Ticker(ticker)
|
||||
news = yf_retry(lambda: stock.get_news(count=20))
|
||||
|
||||
if not news:
|
||||
return f"No news found for {ticker}"
|
||||
|
||||
# Parse date range for filtering
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
news_str = ""
|
||||
filtered_count = 0
|
||||
|
||||
for article in news:
|
||||
data = _extract_article_data(article)
|
||||
|
||||
# Filter by date if publish time is available
|
||||
if data["pub_date"]:
|
||||
pub_date_naive = data["pub_date"].replace(tzinfo=None)
|
||||
if not (start_dt <= pub_date_naive <= end_dt + relativedelta(days=1)):
|
||||
continue
|
||||
|
||||
news_str += f"### {data['title']} (source: {data['publisher']})\n"
|
||||
if data["summary"]:
|
||||
news_str += f"{data['summary']}\n"
|
||||
if data["link"]:
|
||||
news_str += f"Link: {data['link']}\n"
|
||||
news_str += "\n"
|
||||
filtered_count += 1
|
||||
|
||||
if filtered_count == 0:
|
||||
return f"No news found for {ticker} between {start_date} and {end_date}"
|
||||
|
||||
return f"## {ticker} News, from {start_date} to {end_date}:\n\n{news_str}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Error fetching news for {ticker}: {str(e)}"
|
||||
|
||||
|
||||
def get_global_news_yfinance(
|
||||
curr_date: str,
|
||||
look_back_days: int = 7,
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve global/macro economic news using yfinance Search.
|
||||
|
||||
Args:
|
||||
curr_date: Current date in yyyy-mm-dd format
|
||||
look_back_days: Number of days to look back
|
||||
limit: Maximum number of articles to return
|
||||
|
||||
Returns:
|
||||
Formatted string containing global news articles
|
||||
"""
|
||||
# Search queries for macro/global news
|
||||
search_queries = [
|
||||
"stock market economy",
|
||||
"Federal Reserve interest rates",
|
||||
"inflation economic outlook",
|
||||
"global markets trading",
|
||||
]
|
||||
|
||||
all_news = []
|
||||
seen_titles = set()
|
||||
|
||||
try:
|
||||
for query in search_queries:
|
||||
search = yf_retry(lambda q=query: yf.Search(
|
||||
query=q,
|
||||
news_count=limit,
|
||||
enable_fuzzy_query=True,
|
||||
))
|
||||
|
||||
if search.news:
|
||||
for article in search.news:
|
||||
# Handle both flat and nested structures
|
||||
if "content" in article:
|
||||
data = _extract_article_data(article)
|
||||
title = data["title"]
|
||||
else:
|
||||
title = article.get("title", "")
|
||||
|
||||
# Deduplicate by title
|
||||
if title and title not in seen_titles:
|
||||
seen_titles.add(title)
|
||||
all_news.append(article)
|
||||
|
||||
if len(all_news) >= limit:
|
||||
break
|
||||
|
||||
if not all_news:
|
||||
return f"No global news found for {curr_date}"
|
||||
|
||||
# Calculate date range
|
||||
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
||||
start_dt = curr_dt - relativedelta(days=look_back_days)
|
||||
start_date = start_dt.strftime("%Y-%m-%d")
|
||||
|
||||
news_str = ""
|
||||
for article in all_news[:limit]:
|
||||
# Handle both flat and nested structures
|
||||
if "content" in article:
|
||||
data = _extract_article_data(article)
|
||||
# Skip articles published after curr_date (look-ahead guard)
|
||||
if data.get("pub_date"):
|
||||
pub_naive = data["pub_date"].replace(tzinfo=None) if hasattr(data["pub_date"], "replace") else data["pub_date"]
|
||||
if pub_naive > curr_dt + relativedelta(days=1):
|
||||
continue
|
||||
title = data["title"]
|
||||
publisher = data["publisher"]
|
||||
link = data["link"]
|
||||
summary = data["summary"]
|
||||
else:
|
||||
title = article.get("title", "No title")
|
||||
publisher = article.get("publisher", "Unknown")
|
||||
link = article.get("link", "")
|
||||
summary = ""
|
||||
|
||||
news_str += f"### {title} (source: {publisher})\n"
|
||||
if summary:
|
||||
news_str += f"{summary}\n"
|
||||
if link:
|
||||
news_str += f"Link: {link}\n"
|
||||
news_str += "\n"
|
||||
|
||||
return f"## Global Market News, from {start_date} to {curr_date}:\n\n{news_str}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Error fetching global news: {str(e)}"
|
||||
|
|
@ -1,37 +1,19 @@
|
|||
import os
|
||||
|
||||
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
|
||||
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")),
|
||||
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")),
|
||||
"data_dir": "/Users/yluo/Documents/Code/ScAI/FR1-data",
|
||||
"data_cache_dir": os.path.join(
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
|
||||
"dataflows/data_cache",
|
||||
),
|
||||
# LLM settings
|
||||
"llm_provider": "openai",
|
||||
"deep_think_llm": "gpt-5.4",
|
||||
"quick_think_llm": "gpt-5.4-mini",
|
||||
"backend_url": "https://api.openai.com/v1",
|
||||
# Provider-specific thinking configuration
|
||||
"google_thinking_level": None, # "high", "minimal", etc.
|
||||
"openai_reasoning_effort": None, # "medium", "high", "low"
|
||||
"anthropic_effort": None, # "high", "medium", "low"
|
||||
# Output language for analyst reports and final decision
|
||||
# Internal agent debate stays in English for reasoning quality
|
||||
"output_language": "English",
|
||||
"deep_think_llm": "o4-mini",
|
||||
"quick_think_llm": "gpt-4o-mini",
|
||||
# Debate and discussion settings
|
||||
"max_debate_rounds": 1,
|
||||
"max_risk_discuss_rounds": 1,
|
||||
"max_recur_limit": 100,
|
||||
# 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
|
||||
},
|
||||
# Tool-level configuration (takes precedence over category-level)
|
||||
"tool_vendors": {
|
||||
# Example: "get_stock_data": "alpha_vantage", # Override category default
|
||||
},
|
||||
# Tool settings
|
||||
"online_tools": True,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ class ConditionalLogic:
|
|||
if (
|
||||
state["risk_debate_state"]["count"] >= 3 * self.max_risk_discuss_rounds
|
||||
): # 3 rounds of back-and-forth between 3 agents
|
||||
return "Portfolio Manager"
|
||||
if state["risk_debate_state"]["latest_speaker"].startswith("Aggressive"):
|
||||
return "Conservative Analyst"
|
||||
if state["risk_debate_state"]["latest_speaker"].startswith("Conservative"):
|
||||
return "Risk Judge"
|
||||
if state["risk_debate_state"]["latest_speaker"].startswith("Risky"):
|
||||
return "Safe Analyst"
|
||||
if state["risk_debate_state"]["latest_speaker"].startswith("Safe"):
|
||||
return "Neutral Analyst"
|
||||
return "Aggressive Analyst"
|
||||
return "Risky Analyst"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# TradingAgents/graph/propagation.py
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any
|
||||
from tradingagents.agents.utils.agent_states import (
|
||||
AgentState,
|
||||
InvestDebateState,
|
||||
|
|
@ -24,26 +24,14 @@ class Propagator:
|
|||
"company_of_interest": company_name,
|
||||
"trade_date": str(trade_date),
|
||||
"investment_debate_state": InvestDebateState(
|
||||
{
|
||||
"bull_history": "",
|
||||
"bear_history": "",
|
||||
"history": "",
|
||||
"current_response": "",
|
||||
"judge_decision": "",
|
||||
"count": 0,
|
||||
}
|
||||
{"history": "", "current_response": "", "count": 0}
|
||||
),
|
||||
"risk_debate_state": RiskDebateState(
|
||||
{
|
||||
"aggressive_history": "",
|
||||
"conservative_history": "",
|
||||
"neutral_history": "",
|
||||
"history": "",
|
||||
"latest_speaker": "",
|
||||
"current_aggressive_response": "",
|
||||
"current_conservative_response": "",
|
||||
"current_risky_response": "",
|
||||
"current_safe_response": "",
|
||||
"current_neutral_response": "",
|
||||
"judge_decision": "",
|
||||
"count": 0,
|
||||
}
|
||||
),
|
||||
|
|
@ -53,17 +41,9 @@ class Propagator:
|
|||
"news_report": "",
|
||||
}
|
||||
|
||||
def get_graph_args(self, callbacks: Optional[List] = None) -> Dict[str, Any]:
|
||||
"""Get arguments for the graph invocation.
|
||||
|
||||
Args:
|
||||
callbacks: Optional list of callback handlers for tool execution tracking.
|
||||
Note: LLM callbacks are handled separately via LLM constructor.
|
||||
"""
|
||||
config = {"recursion_limit": self.max_recur_limit}
|
||||
if callbacks:
|
||||
config["callbacks"] = callbacks
|
||||
def get_graph_args(self) -> Dict[str, Any]:
|
||||
"""Get arguments for the graph invocation."""
|
||||
return {
|
||||
"stream_mode": "values",
|
||||
"config": config,
|
||||
"config": {"recursion_limit": self.max_recur_limit},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# TradingAgents/graph/reflection.py
|
||||
|
||||
from typing import Any, Dict
|
||||
from typing import Dict, Any
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
class Reflector:
|
||||
"""Handles reflection on decisions and updating memory."""
|
||||
|
||||
def __init__(self, quick_thinking_llm: Any):
|
||||
def __init__(self, quick_thinking_llm: ChatOpenAI):
|
||||
"""Initialize the reflector with an LLM."""
|
||||
self.quick_thinking_llm = quick_thinking_llm
|
||||
self.reflection_system_prompt = self._get_reflection_prompt()
|
||||
|
|
@ -109,12 +110,12 @@ Adhere strictly to these instructions, and ensure your output is detailed, accur
|
|||
)
|
||||
invest_judge_memory.add_situations([(situation, result)])
|
||||
|
||||
def reflect_portfolio_manager(self, current_state, returns_losses, portfolio_manager_memory):
|
||||
"""Reflect on portfolio manager's decision and update memory."""
|
||||
def reflect_risk_manager(self, current_state, returns_losses, risk_manager_memory):
|
||||
"""Reflect on risk manager's decision and update memory."""
|
||||
situation = self._extract_current_situation(current_state)
|
||||
judge_decision = current_state["risk_debate_state"]["judge_decision"]
|
||||
|
||||
result = self._reflect_on_component(
|
||||
"PORTFOLIO MANAGER", judge_decision, situation, returns_losses
|
||||
"RISK JUDGE", judge_decision, situation, returns_losses
|
||||
)
|
||||
portfolio_manager_memory.add_situations([(situation, result)])
|
||||
risk_manager_memory.add_situations([(situation, result)])
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
# TradingAgents/graph/setup.py
|
||||
|
||||
from typing import Any, Dict
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from typing import Dict, Any
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
from tradingagents.agents import *
|
||||
from tradingagents.agents.utils.agent_states import AgentState
|
||||
from tradingagents.agents.utils.agent_utils import Toolkit
|
||||
|
||||
from .conditional_logic import ConditionalLogic
|
||||
|
||||
|
|
@ -15,25 +17,27 @@ class GraphSetup:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
quick_thinking_llm: Any,
|
||||
deep_thinking_llm: Any,
|
||||
quick_thinking_llm: ChatOpenAI,
|
||||
deep_thinking_llm: ChatOpenAI,
|
||||
toolkit: Toolkit,
|
||||
tool_nodes: Dict[str, ToolNode],
|
||||
bull_memory,
|
||||
bear_memory,
|
||||
trader_memory,
|
||||
invest_judge_memory,
|
||||
portfolio_manager_memory,
|
||||
risk_manager_memory,
|
||||
conditional_logic: ConditionalLogic,
|
||||
):
|
||||
"""Initialize with required components."""
|
||||
self.quick_thinking_llm = quick_thinking_llm
|
||||
self.deep_thinking_llm = deep_thinking_llm
|
||||
self.toolkit = toolkit
|
||||
self.tool_nodes = tool_nodes
|
||||
self.bull_memory = bull_memory
|
||||
self.bear_memory = bear_memory
|
||||
self.trader_memory = trader_memory
|
||||
self.invest_judge_memory = invest_judge_memory
|
||||
self.portfolio_manager_memory = portfolio_manager_memory
|
||||
self.risk_manager_memory = risk_manager_memory
|
||||
self.conditional_logic = conditional_logic
|
||||
|
||||
def setup_graph(
|
||||
|
|
@ -58,28 +62,28 @@ class GraphSetup:
|
|||
|
||||
if "market" in selected_analysts:
|
||||
analyst_nodes["market"] = create_market_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.toolkit
|
||||
)
|
||||
delete_nodes["market"] = create_msg_delete()
|
||||
tool_nodes["market"] = self.tool_nodes["market"]
|
||||
|
||||
if "social" in selected_analysts:
|
||||
analyst_nodes["social"] = create_social_media_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.toolkit
|
||||
)
|
||||
delete_nodes["social"] = create_msg_delete()
|
||||
tool_nodes["social"] = self.tool_nodes["social"]
|
||||
|
||||
if "news" in selected_analysts:
|
||||
analyst_nodes["news"] = create_news_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.toolkit
|
||||
)
|
||||
delete_nodes["news"] = create_msg_delete()
|
||||
tool_nodes["news"] = self.tool_nodes["news"]
|
||||
|
||||
if "fundamentals" in selected_analysts:
|
||||
analyst_nodes["fundamentals"] = create_fundamentals_analyst(
|
||||
self.quick_thinking_llm
|
||||
self.quick_thinking_llm, self.toolkit
|
||||
)
|
||||
delete_nodes["fundamentals"] = create_msg_delete()
|
||||
tool_nodes["fundamentals"] = self.tool_nodes["fundamentals"]
|
||||
|
|
@ -97,11 +101,11 @@ class GraphSetup:
|
|||
trader_node = create_trader(self.quick_thinking_llm, self.trader_memory)
|
||||
|
||||
# Create risk analysis nodes
|
||||
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
|
||||
risky_analyst = create_risky_debator(self.quick_thinking_llm)
|
||||
neutral_analyst = create_neutral_debator(self.quick_thinking_llm)
|
||||
conservative_analyst = create_conservative_debator(self.quick_thinking_llm)
|
||||
portfolio_manager_node = create_portfolio_manager(
|
||||
self.deep_thinking_llm, self.portfolio_manager_memory
|
||||
safe_analyst = create_safe_debator(self.quick_thinking_llm)
|
||||
risk_manager_node = create_risk_manager(
|
||||
self.deep_thinking_llm, self.risk_manager_memory
|
||||
)
|
||||
|
||||
# Create workflow
|
||||
|
|
@ -120,10 +124,10 @@ class GraphSetup:
|
|||
workflow.add_node("Bear Researcher", bear_researcher_node)
|
||||
workflow.add_node("Research Manager", research_manager_node)
|
||||
workflow.add_node("Trader", trader_node)
|
||||
workflow.add_node("Aggressive Analyst", aggressive_analyst)
|
||||
workflow.add_node("Risky Analyst", risky_analyst)
|
||||
workflow.add_node("Neutral Analyst", neutral_analyst)
|
||||
workflow.add_node("Conservative Analyst", conservative_analyst)
|
||||
workflow.add_node("Portfolio Manager", portfolio_manager_node)
|
||||
workflow.add_node("Safe Analyst", safe_analyst)
|
||||
workflow.add_node("Risk Judge", risk_manager_node)
|
||||
|
||||
# Define edges
|
||||
# Start with the first analyst
|
||||
|
|
@ -169,33 +173,33 @@ class GraphSetup:
|
|||
},
|
||||
)
|
||||
workflow.add_edge("Research Manager", "Trader")
|
||||
workflow.add_edge("Trader", "Aggressive Analyst")
|
||||
workflow.add_edge("Trader", "Risky Analyst")
|
||||
workflow.add_conditional_edges(
|
||||
"Aggressive Analyst",
|
||||
"Risky Analyst",
|
||||
self.conditional_logic.should_continue_risk_analysis,
|
||||
{
|
||||
"Conservative Analyst": "Conservative Analyst",
|
||||
"Portfolio Manager": "Portfolio Manager",
|
||||
"Safe Analyst": "Safe Analyst",
|
||||
"Risk Judge": "Risk Judge",
|
||||
},
|
||||
)
|
||||
workflow.add_conditional_edges(
|
||||
"Conservative Analyst",
|
||||
"Safe Analyst",
|
||||
self.conditional_logic.should_continue_risk_analysis,
|
||||
{
|
||||
"Neutral Analyst": "Neutral Analyst",
|
||||
"Portfolio Manager": "Portfolio Manager",
|
||||
"Risk Judge": "Risk Judge",
|
||||
},
|
||||
)
|
||||
workflow.add_conditional_edges(
|
||||
"Neutral Analyst",
|
||||
self.conditional_logic.should_continue_risk_analysis,
|
||||
{
|
||||
"Aggressive Analyst": "Aggressive Analyst",
|
||||
"Portfolio Manager": "Portfolio Manager",
|
||||
"Risky Analyst": "Risky Analyst",
|
||||
"Risk Judge": "Risk Judge",
|
||||
},
|
||||
)
|
||||
|
||||
workflow.add_edge("Portfolio Manager", END)
|
||||
workflow.add_edge("Risk Judge", END)
|
||||
|
||||
# Compile and return
|
||||
return workflow.compile()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# TradingAgents/graph/signal_processing.py
|
||||
|
||||
from typing import Any
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
class SignalProcessor:
|
||||
"""Processes trading signals to extract actionable decisions."""
|
||||
|
||||
def __init__(self, quick_thinking_llm: Any):
|
||||
def __init__(self, quick_thinking_llm: ChatOpenAI):
|
||||
"""Initialize with an LLM for processing."""
|
||||
self.quick_thinking_llm = quick_thinking_llm
|
||||
|
||||
|
|
@ -18,14 +18,12 @@ class SignalProcessor:
|
|||
full_signal: Complete trading signal text
|
||||
|
||||
Returns:
|
||||
Extracted rating (BUY, OVERWEIGHT, HOLD, UNDERWEIGHT, or SELL)
|
||||
Extracted decision (BUY, SELL, or HOLD)
|
||||
"""
|
||||
messages = [
|
||||
(
|
||||
"system",
|
||||
"You are an efficient assistant that extracts the trading decision from analyst reports. "
|
||||
"Extract the rating as exactly one of: BUY, OVERWEIGHT, HOLD, UNDERWEIGHT, SELL. "
|
||||
"Output only the single rating word, nothing else.",
|
||||
"You are an efficient assistant designed to analyze paragraphs or financial reports provided by a group of analysts. Your task is to extract the investment decision: SELL, BUY, or HOLD. Provide only the extracted decision (SELL, BUY, or HOLD) as your output, without adding any additional text or information.",
|
||||
),
|
||||
("human", full_signal),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ import json
|
|||
from datetime import date
|
||||
from typing import Dict, Any, Tuple, List, Optional
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
from tradingagents.llm_clients import create_llm_client
|
||||
|
||||
from tradingagents.agents import *
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.agents.utils.memory import FinancialSituationMemory
|
||||
|
|
@ -18,20 +17,7 @@ from tradingagents.agents.utils.agent_states import (
|
|||
InvestDebateState,
|
||||
RiskDebateState,
|
||||
)
|
||||
from tradingagents.dataflows.config import set_config
|
||||
|
||||
# Import the new abstract tool methods from agent_utils
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
get_stock_data,
|
||||
get_indicators,
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_income_statement,
|
||||
get_news,
|
||||
get_insider_transactions,
|
||||
get_global_news
|
||||
)
|
||||
from tradingagents.dataflows.interface import set_config
|
||||
|
||||
from .conditional_logic import ConditionalLogic
|
||||
from .setup import GraphSetup
|
||||
|
|
@ -48,7 +34,6 @@ class TradingAgentsGraph:
|
|||
selected_analysts=["market", "social", "news", "fundamentals"],
|
||||
debug=False,
|
||||
config: Dict[str, Any] = None,
|
||||
callbacks: Optional[List] = None,
|
||||
):
|
||||
"""Initialize the trading agents graph and components.
|
||||
|
||||
|
|
@ -56,66 +41,48 @@ class TradingAgentsGraph:
|
|||
selected_analysts: List of analyst types to include
|
||||
debug: Whether to run in debug mode
|
||||
config: Configuration dictionary. If None, uses default config
|
||||
callbacks: Optional list of callback handlers (e.g., for tracking LLM/tool stats)
|
||||
"""
|
||||
self.debug = debug
|
||||
self.config = config or DEFAULT_CONFIG
|
||||
self.callbacks = callbacks or []
|
||||
|
||||
# Update the interface's config
|
||||
set_config(self.config)
|
||||
|
||||
# Create necessary directories
|
||||
os.makedirs(self.config["data_cache_dir"], exist_ok=True)
|
||||
os.makedirs(self.config["results_dir"], exist_ok=True)
|
||||
|
||||
# Initialize LLMs with provider-specific thinking configuration
|
||||
llm_kwargs = self._get_provider_kwargs()
|
||||
|
||||
# Add callbacks to kwargs if provided (passed to LLM constructor)
|
||||
if self.callbacks:
|
||||
llm_kwargs["callbacks"] = self.callbacks
|
||||
|
||||
deep_client = create_llm_client(
|
||||
provider=self.config["llm_provider"],
|
||||
model=self.config["deep_think_llm"],
|
||||
base_url=self.config.get("backend_url"),
|
||||
**llm_kwargs,
|
||||
)
|
||||
quick_client = create_llm_client(
|
||||
provider=self.config["llm_provider"],
|
||||
model=self.config["quick_think_llm"],
|
||||
base_url=self.config.get("backend_url"),
|
||||
**llm_kwargs,
|
||||
os.makedirs(
|
||||
os.path.join(self.config["project_dir"], "dataflows/data_cache"),
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
self.deep_thinking_llm = deep_client.get_llm()
|
||||
self.quick_thinking_llm = quick_client.get_llm()
|
||||
|
||||
# Initialize LLMs
|
||||
self.deep_thinking_llm = ChatOpenAI(model=self.config["deep_think_llm"])
|
||||
self.quick_thinking_llm = ChatOpenAI(
|
||||
model=self.config["quick_think_llm"], temperature=0.1
|
||||
)
|
||||
self.toolkit = Toolkit(config=self.config)
|
||||
|
||||
# Initialize memories
|
||||
self.bull_memory = FinancialSituationMemory("bull_memory", self.config)
|
||||
self.bear_memory = FinancialSituationMemory("bear_memory", self.config)
|
||||
self.trader_memory = FinancialSituationMemory("trader_memory", self.config)
|
||||
self.invest_judge_memory = FinancialSituationMemory("invest_judge_memory", self.config)
|
||||
self.portfolio_manager_memory = FinancialSituationMemory("portfolio_manager_memory", self.config)
|
||||
self.bull_memory = FinancialSituationMemory("bull_memory")
|
||||
self.bear_memory = FinancialSituationMemory("bear_memory")
|
||||
self.trader_memory = FinancialSituationMemory("trader_memory")
|
||||
self.invest_judge_memory = FinancialSituationMemory("invest_judge_memory")
|
||||
self.risk_manager_memory = FinancialSituationMemory("risk_manager_memory")
|
||||
|
||||
# Create tool nodes
|
||||
self.tool_nodes = self._create_tool_nodes()
|
||||
|
||||
# Initialize components
|
||||
self.conditional_logic = ConditionalLogic(
|
||||
max_debate_rounds=self.config["max_debate_rounds"],
|
||||
max_risk_discuss_rounds=self.config["max_risk_discuss_rounds"],
|
||||
)
|
||||
self.conditional_logic = ConditionalLogic()
|
||||
self.graph_setup = GraphSetup(
|
||||
self.quick_thinking_llm,
|
||||
self.deep_thinking_llm,
|
||||
self.toolkit,
|
||||
self.tool_nodes,
|
||||
self.bull_memory,
|
||||
self.bear_memory,
|
||||
self.trader_memory,
|
||||
self.invest_judge_memory,
|
||||
self.portfolio_manager_memory,
|
||||
self.risk_manager_memory,
|
||||
self.conditional_logic,
|
||||
)
|
||||
|
||||
|
|
@ -131,60 +98,47 @@ class TradingAgentsGraph:
|
|||
# Set up the graph
|
||||
self.graph = self.graph_setup.setup_graph(selected_analysts)
|
||||
|
||||
def _get_provider_kwargs(self) -> Dict[str, Any]:
|
||||
"""Get provider-specific kwargs for LLM client creation."""
|
||||
kwargs = {}
|
||||
provider = self.config.get("llm_provider", "").lower()
|
||||
|
||||
if provider == "google":
|
||||
thinking_level = self.config.get("google_thinking_level")
|
||||
if thinking_level:
|
||||
kwargs["thinking_level"] = thinking_level
|
||||
|
||||
elif provider == "openai":
|
||||
reasoning_effort = self.config.get("openai_reasoning_effort")
|
||||
if reasoning_effort:
|
||||
kwargs["reasoning_effort"] = reasoning_effort
|
||||
|
||||
elif provider == "anthropic":
|
||||
effort = self.config.get("anthropic_effort")
|
||||
if effort:
|
||||
kwargs["effort"] = effort
|
||||
|
||||
return kwargs
|
||||
|
||||
def _create_tool_nodes(self) -> Dict[str, ToolNode]:
|
||||
"""Create tool nodes for different data sources using abstract methods."""
|
||||
"""Create tool nodes for different data sources."""
|
||||
return {
|
||||
"market": ToolNode(
|
||||
[
|
||||
# Core stock data tools
|
||||
get_stock_data,
|
||||
# Technical indicators
|
||||
get_indicators,
|
||||
# online tools
|
||||
self.toolkit.get_YFin_data_online,
|
||||
self.toolkit.get_stockstats_indicators_report_online,
|
||||
# offline tools
|
||||
self.toolkit.get_YFin_data,
|
||||
self.toolkit.get_stockstats_indicators_report,
|
||||
]
|
||||
),
|
||||
"social": ToolNode(
|
||||
[
|
||||
# News tools for social media analysis
|
||||
get_news,
|
||||
# online tools
|
||||
self.toolkit.get_stock_news_openai,
|
||||
# offline tools
|
||||
self.toolkit.get_reddit_stock_info,
|
||||
]
|
||||
),
|
||||
"news": ToolNode(
|
||||
[
|
||||
# News and insider information
|
||||
get_news,
|
||||
get_global_news,
|
||||
get_insider_transactions,
|
||||
# online tools
|
||||
self.toolkit.get_global_news_openai,
|
||||
self.toolkit.get_google_news,
|
||||
# offline tools
|
||||
self.toolkit.get_finnhub_news,
|
||||
self.toolkit.get_reddit_news,
|
||||
]
|
||||
),
|
||||
"fundamentals": ToolNode(
|
||||
[
|
||||
# Fundamental analysis tools
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_income_statement,
|
||||
# online tools
|
||||
self.toolkit.get_fundamentals_openai,
|
||||
# offline tools
|
||||
self.toolkit.get_finnhub_company_insider_sentiment,
|
||||
self.toolkit.get_finnhub_company_insider_transactions,
|
||||
self.toolkit.get_simfin_balance_sheet,
|
||||
self.toolkit.get_simfin_cashflow,
|
||||
self.toolkit.get_simfin_income_stmt,
|
||||
]
|
||||
),
|
||||
}
|
||||
|
|
@ -246,8 +200,8 @@ class TradingAgentsGraph:
|
|||
},
|
||||
"trader_investment_decision": final_state["trader_investment_plan"],
|
||||
"risk_debate_state": {
|
||||
"aggressive_history": final_state["risk_debate_state"]["aggressive_history"],
|
||||
"conservative_history": final_state["risk_debate_state"]["conservative_history"],
|
||||
"risky_history": final_state["risk_debate_state"]["risky_history"],
|
||||
"safe_history": final_state["risk_debate_state"]["safe_history"],
|
||||
"neutral_history": final_state["risk_debate_state"]["neutral_history"],
|
||||
"history": final_state["risk_debate_state"]["history"],
|
||||
"judge_decision": final_state["risk_debate_state"]["judge_decision"],
|
||||
|
|
@ -257,12 +211,14 @@ class TradingAgentsGraph:
|
|||
}
|
||||
|
||||
# Save to file
|
||||
directory = Path(self.config["results_dir"]) / self.ticker / "TradingAgentsStrategy_logs"
|
||||
directory = Path(f"eval_results/{self.ticker}/TradingAgentsStrategy_logs/")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log_path = directory / f"full_states_log_{trade_date}.json"
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.log_states_dict[str(trade_date)], f, indent=4)
|
||||
with open(
|
||||
f"eval_results/{self.ticker}/TradingAgentsStrategy_logs/full_states_log.json",
|
||||
"w",
|
||||
) as f:
|
||||
json.dump(self.log_states_dict, f, indent=4)
|
||||
|
||||
def reflect_and_remember(self, returns_losses):
|
||||
"""Reflect on decisions and update memory based on returns."""
|
||||
|
|
@ -278,8 +234,8 @@ class TradingAgentsGraph:
|
|||
self.reflector.reflect_invest_judge(
|
||||
self.curr_state, returns_losses, self.invest_judge_memory
|
||||
)
|
||||
self.reflector.reflect_portfolio_manager(
|
||||
self.curr_state, returns_losses, self.portfolio_manager_memory
|
||||
self.reflector.reflect_risk_manager(
|
||||
self.curr_state, returns_losses, self.risk_manager_memory
|
||||
)
|
||||
|
||||
def process_signal(self, full_signal):
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# LLM Clients - Consistency Improvements
|
||||
|
||||
## Issues to Fix
|
||||
|
||||
### 1. `validate_model()` is never called
|
||||
- Add validation call in `get_llm()` with warning (not error) for unknown models
|
||||
|
||||
### 2. ~~Inconsistent parameter handling~~ (Fixed)
|
||||
- GoogleClient now accepts unified `api_key` and maps it to `google_api_key`
|
||||
|
||||
### 3. ~~`base_url` accepted but ignored~~ (Fixed)
|
||||
- All clients now pass `base_url` to their respective LLM constructors
|
||||
|
||||
### 4. ~~Update validators.py with models from CLI~~ (Fixed)
|
||||
- Synced in v0.2.2
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from .base_client import BaseLLMClient
|
||||
from .factory import create_llm_client
|
||||
|
||||
__all__ = ["BaseLLMClient", "create_llm_client"]
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
from .base_client import BaseLLMClient, normalize_content
|
||||
from .validators import validate_model
|
||||
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "api_key", "max_tokens",
|
||||
"callbacks", "http_client", "http_async_client", "effort",
|
||||
)
|
||||
|
||||
|
||||
class NormalizedChatAnthropic(ChatAnthropic):
|
||||
"""ChatAnthropic with normalized content output.
|
||||
|
||||
Claude models with extended thinking or tool use return content as a
|
||||
list of typed blocks. This normalizes to string for consistent
|
||||
downstream handling.
|
||||
"""
|
||||
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
|
||||
class AnthropicClient(BaseLLMClient):
|
||||
"""Client for Anthropic Claude models."""
|
||||
|
||||
def __init__(self, model: str, base_url: Optional[str] = None, **kwargs):
|
||||
super().__init__(model, base_url, **kwargs)
|
||||
|
||||
def get_llm(self) -> Any:
|
||||
"""Return configured ChatAnthropic instance."""
|
||||
self.warn_if_unknown_model()
|
||||
llm_kwargs = {"model": self.model}
|
||||
|
||||
if self.base_url:
|
||||
llm_kwargs["base_url"] = self.base_url
|
||||
|
||||
for key in _PASSTHROUGH_KWARGS:
|
||||
if key in self.kwargs:
|
||||
llm_kwargs[key] = self.kwargs[key]
|
||||
|
||||
return NormalizedChatAnthropic(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
"""Validate model for Anthropic."""
|
||||
return validate_model("anthropic", self.model)
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
|
||||
from .base_client import BaseLLMClient, normalize_content
|
||||
from .validators import validate_model
|
||||
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "api_key", "reasoning_effort",
|
||||
"callbacks", "http_client", "http_async_client",
|
||||
)
|
||||
|
||||
|
||||
class NormalizedAzureChatOpenAI(AzureChatOpenAI):
|
||||
"""AzureChatOpenAI with normalized content output."""
|
||||
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
|
||||
class AzureOpenAIClient(BaseLLMClient):
|
||||
"""Client for Azure OpenAI deployments.
|
||||
|
||||
Requires environment variables:
|
||||
AZURE_OPENAI_API_KEY: API key
|
||||
AZURE_OPENAI_ENDPOINT: Endpoint URL (e.g. https://<resource>.openai.azure.com/)
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: Deployment name
|
||||
OPENAI_API_VERSION: API version (e.g. 2025-03-01-preview)
|
||||
"""
|
||||
|
||||
def __init__(self, model: str, base_url: Optional[str] = None, **kwargs):
|
||||
super().__init__(model, base_url, **kwargs)
|
||||
|
||||
def get_llm(self) -> Any:
|
||||
"""Return configured AzureChatOpenAI instance."""
|
||||
self.warn_if_unknown_model()
|
||||
|
||||
llm_kwargs = {
|
||||
"model": self.model,
|
||||
"azure_deployment": os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", self.model),
|
||||
}
|
||||
|
||||
for key in _PASSTHROUGH_KWARGS:
|
||||
if key in self.kwargs:
|
||||
llm_kwargs[key] = self.kwargs[key]
|
||||
|
||||
return NormalizedAzureChatOpenAI(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
"""Azure accepts any deployed model name."""
|
||||
return True
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Optional
|
||||
import warnings
|
||||
|
||||
|
||||
def normalize_content(response):
|
||||
"""Normalize LLM response content to a plain string.
|
||||
|
||||
Multiple providers (OpenAI Responses API, Google Gemini 3) return content
|
||||
as a list of typed blocks, e.g. [{'type': 'reasoning', ...}, {'type': 'text', 'text': '...'}].
|
||||
Downstream agents expect response.content to be a string. This extracts
|
||||
and joins the text blocks, discarding reasoning/metadata blocks.
|
||||
"""
|
||||
content = response.content
|
||||
if isinstance(content, list):
|
||||
texts = [
|
||||
item.get("text", "") if isinstance(item, dict) and item.get("type") == "text"
|
||||
else item if isinstance(item, str) else ""
|
||||
for item in content
|
||||
]
|
||||
response.content = "\n".join(t for t in texts if t)
|
||||
return response
|
||||
|
||||
|
||||
class BaseLLMClient(ABC):
|
||||
"""Abstract base class for LLM clients."""
|
||||
|
||||
def __init__(self, model: str, base_url: Optional[str] = None, **kwargs):
|
||||
self.model = model
|
||||
self.base_url = base_url
|
||||
self.kwargs = kwargs
|
||||
|
||||
def get_provider_name(self) -> str:
|
||||
"""Return the provider name used in warning messages."""
|
||||
provider = getattr(self, "provider", None)
|
||||
if provider:
|
||||
return str(provider)
|
||||
return self.__class__.__name__.removesuffix("Client").lower()
|
||||
|
||||
def warn_if_unknown_model(self) -> None:
|
||||
"""Warn when the model is outside the known list for the provider."""
|
||||
if self.validate_model():
|
||||
return
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
f"Model '{self.model}' is not in the known model list for "
|
||||
f"provider '{self.get_provider_name()}'. Continuing anyway."
|
||||
),
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_llm(self) -> Any:
|
||||
"""Return the configured LLM instance."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_model(self) -> bool:
|
||||
"""Validate that the model is supported by this client."""
|
||||
pass
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from .base_client import BaseLLMClient
|
||||
from .openai_client import OpenAIClient
|
||||
from .anthropic_client import AnthropicClient
|
||||
from .google_client import GoogleClient
|
||||
from .azure_client import AzureOpenAIClient
|
||||
|
||||
# Providers that use the OpenAI-compatible chat completions API
|
||||
_OPENAI_COMPATIBLE = (
|
||||
"openai", "xai", "deepseek", "qwen", "glm", "ollama", "openrouter",
|
||||
)
|
||||
|
||||
|
||||
def create_llm_client(
|
||||
provider: str,
|
||||
model: str,
|
||||
base_url: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> BaseLLMClient:
|
||||
"""Create an LLM client for the specified provider.
|
||||
|
||||
Args:
|
||||
provider: LLM provider name
|
||||
model: Model name/identifier
|
||||
base_url: Optional base URL for API endpoint
|
||||
**kwargs: Additional provider-specific arguments
|
||||
|
||||
Returns:
|
||||
Configured BaseLLMClient instance
|
||||
|
||||
Raises:
|
||||
ValueError: If provider is not supported
|
||||
"""
|
||||
provider_lower = provider.lower()
|
||||
|
||||
if provider_lower in _OPENAI_COMPATIBLE:
|
||||
return OpenAIClient(model, base_url, provider=provider_lower, **kwargs)
|
||||
|
||||
if provider_lower == "anthropic":
|
||||
return AnthropicClient(model, base_url, **kwargs)
|
||||
|
||||
if provider_lower == "google":
|
||||
return GoogleClient(model, base_url, **kwargs)
|
||||
|
||||
if provider_lower == "azure":
|
||||
return AzureOpenAIClient(model, base_url, **kwargs)
|
||||
|
||||
raise ValueError(f"Unsupported LLM provider: {provider}")
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
|
||||
from .base_client import BaseLLMClient, normalize_content
|
||||
from .validators import validate_model
|
||||
|
||||
|
||||
class NormalizedChatGoogleGenerativeAI(ChatGoogleGenerativeAI):
|
||||
"""ChatGoogleGenerativeAI with normalized content output.
|
||||
|
||||
Gemini 3 models return content as list of typed blocks.
|
||||
This normalizes to string for consistent downstream handling.
|
||||
"""
|
||||
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
|
||||
class GoogleClient(BaseLLMClient):
|
||||
"""Client for Google Gemini models."""
|
||||
|
||||
def __init__(self, model: str, base_url: Optional[str] = None, **kwargs):
|
||||
super().__init__(model, base_url, **kwargs)
|
||||
|
||||
def get_llm(self) -> Any:
|
||||
"""Return configured ChatGoogleGenerativeAI instance."""
|
||||
self.warn_if_unknown_model()
|
||||
llm_kwargs = {"model": self.model}
|
||||
|
||||
if self.base_url:
|
||||
llm_kwargs["base_url"] = self.base_url
|
||||
|
||||
for key in ("timeout", "max_retries", "callbacks", "http_client", "http_async_client"):
|
||||
if key in self.kwargs:
|
||||
llm_kwargs[key] = self.kwargs[key]
|
||||
|
||||
# Unified api_key maps to provider-specific google_api_key
|
||||
google_api_key = self.kwargs.get("api_key") or self.kwargs.get("google_api_key")
|
||||
if google_api_key:
|
||||
llm_kwargs["google_api_key"] = google_api_key
|
||||
|
||||
# Map thinking_level to appropriate API param based on model
|
||||
# Gemini 3 Pro: low, high
|
||||
# Gemini 3 Flash: minimal, low, medium, high
|
||||
# Gemini 2.5: thinking_budget (0=disable, -1=dynamic)
|
||||
thinking_level = self.kwargs.get("thinking_level")
|
||||
if thinking_level:
|
||||
model_lower = self.model.lower()
|
||||
if "gemini-3" in model_lower:
|
||||
# Gemini 3 Pro doesn't support "minimal", use "low" instead
|
||||
if "pro" in model_lower and thinking_level == "minimal":
|
||||
thinking_level = "low"
|
||||
llm_kwargs["thinking_level"] = thinking_level
|
||||
else:
|
||||
# Gemini 2.5: map to thinking_budget
|
||||
llm_kwargs["thinking_budget"] = -1 if thinking_level == "high" else 0
|
||||
|
||||
return NormalizedChatGoogleGenerativeAI(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
"""Validate model for Google."""
|
||||
return validate_model("google", self.model)
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
"""Shared model catalog for CLI selections and validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
ModelOption = Tuple[str, str]
|
||||
ProviderModeOptions = Dict[str, Dict[str, List[ModelOption]]]
|
||||
|
||||
|
||||
MODEL_OPTIONS: ProviderModeOptions = {
|
||||
"openai": {
|
||||
"quick": [
|
||||
("GPT-5.4 Mini - Fast, strong coding and tool use", "gpt-5.4-mini"),
|
||||
("GPT-5.4 Nano - Cheapest, high-volume tasks", "gpt-5.4-nano"),
|
||||
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
|
||||
("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"),
|
||||
],
|
||||
"deep": [
|
||||
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
|
||||
("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"),
|
||||
("GPT-5.4 Mini - Fast, strong coding and tool use", "gpt-5.4-mini"),
|
||||
("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"),
|
||||
],
|
||||
},
|
||||
"anthropic": {
|
||||
"quick": [
|
||||
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
|
||||
("Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"),
|
||||
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
|
||||
],
|
||||
"deep": [
|
||||
("Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"),
|
||||
("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"),
|
||||
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
|
||||
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
|
||||
],
|
||||
},
|
||||
"google": {
|
||||
"quick": [
|
||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
|
||||
("Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"),
|
||||
("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"),
|
||||
],
|
||||
"deep": [
|
||||
("Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"),
|
||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||
("Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"),
|
||||
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
|
||||
],
|
||||
},
|
||||
"xai": {
|
||||
"quick": [
|
||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||
],
|
||||
"deep": [
|
||||
("Grok 4 - Flagship model", "grok-4-0709"),
|
||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||
],
|
||||
},
|
||||
"deepseek": {
|
||||
"quick": [
|
||||
("DeepSeek V3.2", "deepseek-chat"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
"deep": [
|
||||
("DeepSeek V3.2 (thinking)", "deepseek-reasoner"),
|
||||
("DeepSeek V3.2", "deepseek-chat"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
},
|
||||
"qwen": {
|
||||
"quick": [
|
||||
("Qwen 3.5 Flash", "qwen3.5-flash"),
|
||||
("Qwen Plus", "qwen-plus"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
"deep": [
|
||||
("Qwen 3.6 Plus", "qwen3.6-plus"),
|
||||
("Qwen 3.5 Plus", "qwen3.5-plus"),
|
||||
("Qwen 3 Max", "qwen3-max"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
},
|
||||
"glm": {
|
||||
"quick": [
|
||||
("GLM-4.7", "glm-4.7"),
|
||||
("GLM-5", "glm-5"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
"deep": [
|
||||
("GLM-5.1", "glm-5.1"),
|
||||
("GLM-5", "glm-5"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
},
|
||||
# OpenRouter: fetched dynamically. Azure: any deployed model name.
|
||||
"ollama": {
|
||||
"quick": [
|
||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
||||
],
|
||||
"deep": [
|
||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_model_options(provider: str, mode: str) -> List[ModelOption]:
|
||||
"""Return shared model options for a provider and selection mode."""
|
||||
return MODEL_OPTIONS[provider.lower()][mode]
|
||||
|
||||
|
||||
def get_known_models() -> Dict[str, List[str]]:
|
||||
"""Build known model names from the shared CLI catalog."""
|
||||
return {
|
||||
provider: sorted(
|
||||
{
|
||||
value
|
||||
for options in mode_options.values()
|
||||
for _, value in options
|
||||
}
|
||||
)
|
||||
for provider, mode_options in MODEL_OPTIONS.items()
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from .base_client import BaseLLMClient, normalize_content
|
||||
from .validators import validate_model
|
||||
|
||||
|
||||
class NormalizedChatOpenAI(ChatOpenAI):
|
||||
"""ChatOpenAI with normalized content output.
|
||||
|
||||
The Responses API returns content as a list of typed blocks
|
||||
(reasoning, text, etc.). This normalizes to string for consistent
|
||||
downstream handling.
|
||||
"""
|
||||
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
# Kwargs forwarded from user config to ChatOpenAI
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "reasoning_effort",
|
||||
"api_key", "callbacks", "http_client", "http_async_client",
|
||||
)
|
||||
|
||||
# Provider base URLs and API key env vars
|
||||
_PROVIDER_CONFIG = {
|
||||
"xai": ("https://api.x.ai/v1", "XAI_API_KEY"),
|
||||
"deepseek": ("https://api.deepseek.com", "DEEPSEEK_API_KEY"),
|
||||
"qwen": ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "DASHSCOPE_API_KEY"),
|
||||
"glm": ("https://api.z.ai/api/paas/v4/", "ZHIPU_API_KEY"),
|
||||
"openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
|
||||
"ollama": ("http://localhost:11434/v1", None),
|
||||
}
|
||||
|
||||
|
||||
class OpenAIClient(BaseLLMClient):
|
||||
"""Client for OpenAI, Ollama, OpenRouter, and xAI providers.
|
||||
|
||||
For native OpenAI models, uses the Responses API (/v1/responses) which
|
||||
supports reasoning_effort with function tools across all model families
|
||||
(GPT-4.1, GPT-5). Third-party compatible providers (xAI, OpenRouter,
|
||||
Ollama) use standard Chat Completions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
base_url: Optional[str] = None,
|
||||
provider: str = "openai",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(model, base_url, **kwargs)
|
||||
self.provider = provider.lower()
|
||||
|
||||
def get_llm(self) -> Any:
|
||||
"""Return configured ChatOpenAI instance."""
|
||||
self.warn_if_unknown_model()
|
||||
llm_kwargs = {"model": self.model}
|
||||
|
||||
# Provider-specific base URL and auth
|
||||
if self.provider in _PROVIDER_CONFIG:
|
||||
base_url, api_key_env = _PROVIDER_CONFIG[self.provider]
|
||||
llm_kwargs["base_url"] = base_url
|
||||
if api_key_env:
|
||||
api_key = os.environ.get(api_key_env)
|
||||
if api_key:
|
||||
llm_kwargs["api_key"] = api_key
|
||||
else:
|
||||
llm_kwargs["api_key"] = "ollama"
|
||||
elif self.base_url:
|
||||
llm_kwargs["base_url"] = self.base_url
|
||||
|
||||
# Forward user-provided kwargs
|
||||
for key in _PASSTHROUGH_KWARGS:
|
||||
if key in self.kwargs:
|
||||
llm_kwargs[key] = self.kwargs[key]
|
||||
|
||||
# Native OpenAI: use Responses API for consistent behavior across
|
||||
# all model families. Third-party providers use Chat Completions.
|
||||
if self.provider == "openai":
|
||||
llm_kwargs["use_responses_api"] = True
|
||||
|
||||
return NormalizedChatOpenAI(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
"""Validate model for the provider."""
|
||||
return validate_model(self.provider, self.model)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"""Model name validators for each provider."""
|
||||
|
||||
from .model_catalog import get_known_models
|
||||
|
||||
|
||||
VALID_MODELS = {
|
||||
provider: models
|
||||
for provider, models in get_known_models().items()
|
||||
if provider not in ("ollama", "openrouter")
|
||||
}
|
||||
|
||||
|
||||
def validate_model(provider: str, model: str) -> bool:
|
||||
"""Check if model name is valid for the given provider.
|
||||
|
||||
For ollama, openrouter - any model is accepted.
|
||||
"""
|
||||
provider_lower = provider.lower()
|
||||
|
||||
if provider_lower in ("ollama", "openrouter"):
|
||||
return True
|
||||
|
||||
if provider_lower not in VALID_MODELS:
|
||||
return True
|
||||
|
||||
return model in VALID_MODELS[provider_lower]
|
||||
Loading…
Reference in New Issue