mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
build: require Python 3.11 and pandas 3
- requires-python 3.11 and pandas 3.0.6; the CI matrix runs Python 3.11 to 3.14 and ruff targets 3.11 - datetime.UTC in place of timezone.utc, and the rating, action, band, analyst and asset enums are StrEnum, which formats a member as its value
This commit is contained in:
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
# One run away from UTC, so a test that only passes in UTC fails here.
|
||||
include:
|
||||
- python-version: "3.12"
|
||||
|
||||
@@ -119,7 +119,7 @@ git clone https://github.com/TauricResearch/TradingAgents.git
|
||||
cd TradingAgents
|
||||
```
|
||||
|
||||
Create a virtual environment in any of your favorite environment managers:
|
||||
TradingAgents needs Python 3.11 or later. Create a virtual environment in any of your favorite environment managers:
|
||||
```bash
|
||||
conda create -n tradingagents python=3.13
|
||||
conda activate tradingagents
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class AnalystType(str, Enum):
|
||||
class AnalystType(StrEnum):
|
||||
MARKET = "market"
|
||||
# Wire value stays "social" for saved-config and string-keyed-caller
|
||||
# back-compat; the user-facing label is "Sentiment Analyst".
|
||||
@@ -10,6 +10,6 @@ class AnalystType(str, Enum):
|
||||
FUNDAMENTALS = "fundamentals"
|
||||
|
||||
|
||||
class AssetType(str, Enum):
|
||||
class AssetType(StrEnum):
|
||||
STOCK = "stock"
|
||||
CRYPTO = "crypto"
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ name = "tradingagents"
|
||||
dynamic = ["version"]
|
||||
description = "TradingAgents: Multi-Agents LLM Financial Trading Framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"langchain-core>=1.6.5",
|
||||
"langchain-anthropic>=1.7.4",
|
||||
@@ -15,7 +15,7 @@ dependencies = [
|
||||
"langchain-openai>=1.6.6",
|
||||
"langgraph>=1.2.12",
|
||||
"langgraph-checkpoint-sqlite>=3.1.1",
|
||||
"pandas>=2.3.0",
|
||||
"pandas>=3.0.6",
|
||||
"python-dotenv>=1.0.0",
|
||||
"pytz>=2025.2",
|
||||
"questionary>=2.1.0",
|
||||
@@ -65,7 +65,7 @@ filterwarnings = [
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
target-version = "py311"
|
||||
extend-exclude = ["results"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
@@ -6,7 +6,7 @@ news injected future articles), #993 (empty-after-filter returned a blank body),
|
||||
and #1126 (inclusive upper bound leaked the midnight-after article; host-local
|
||||
timestamp parsing made filtering machine-dependent).
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -16,7 +16,7 @@ from tradingagents.dataflows.date_window import in_window
|
||||
|
||||
def _epoch(date_str):
|
||||
"""Epoch seconds for UTC midnight of ``date_str`` (host-timezone independent)."""
|
||||
return int(datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp())
|
||||
return int(datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC).timestamp())
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -28,7 +28,7 @@ def test_flat_article_publish_time_is_parsed():
|
||||
)
|
||||
assert data["pub_date"] is not None
|
||||
assert data["pub_date"].tzinfo is not None
|
||||
assert data["pub_date"] == datetime(2025, 5, 9, tzinfo=timezone.utc)
|
||||
assert data["pub_date"] == datetime(2025, 5, 9, tzinfo=UTC)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -45,7 +45,7 @@ def test_window_excludes_future_and_undated_in_backtest():
|
||||
@pytest.mark.unit
|
||||
def test_window_keeps_undated_in_live_window():
|
||||
# Live window (reaches today): undated articles can't be "future", so keep them.
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
assert in_window(None, now, now) is True
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ def test_upper_bound_is_exclusive():
|
||||
# the old inclusive bound; the whole of end_date itself must still be kept.
|
||||
start = datetime(2025, 5, 1)
|
||||
end = datetime(2025, 5, 9)
|
||||
midnight_after = datetime(2025, 5, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
last_moment = datetime(2025, 5, 9, 23, 59, 59, tzinfo=timezone.utc)
|
||||
midnight_after = datetime(2025, 5, 10, 0, 0, 0, tzinfo=UTC)
|
||||
last_moment = datetime(2025, 5, 9, 23, 59, 59, tzinfo=UTC)
|
||||
assert in_window(midnight_after, start, end) is False
|
||||
assert in_window(last_moment, start, end) is True
|
||||
|
||||
@@ -146,11 +146,11 @@ def test_ticker_news_covered_but_empty_window_is_a_real_absence(monkeypatch):
|
||||
@pytest.mark.parametrize("dates, expect_gap", [
|
||||
([], True), # empty feed: covers at most now
|
||||
([None], True), # undated only: same
|
||||
([datetime(2026, 5, 20, tzinfo=timezone.utc)], True), # all after the window
|
||||
([datetime(2026, 5, 4, tzinfo=timezone.utc)], True), # starts mid-window: partial
|
||||
([datetime(2026, 5, 1, 18, tzinfo=timezone.utc)], False), # reaches the first day
|
||||
([datetime(2026, 5, 20, tzinfo=timezone.utc),
|
||||
datetime(2026, 4, 1, tzinfo=timezone.utc)], False), # coverage reaches back
|
||||
([datetime(2026, 5, 20, tzinfo=UTC)], True), # all after the window
|
||||
([datetime(2026, 5, 4, tzinfo=UTC)], True), # starts mid-window: partial
|
||||
([datetime(2026, 5, 1, 18, tzinfo=UTC)], False), # reaches the first day
|
||||
([datetime(2026, 5, 20, tzinfo=UTC),
|
||||
datetime(2026, 4, 1, tzinfo=UTC)], False), # coverage reaches back
|
||||
])
|
||||
def test_coverage_gap_boundaries(dates, expect_gap):
|
||||
from tradingagents.dataflows.date_window import coverage_gap
|
||||
@@ -208,7 +208,7 @@ def test_coverage_gap_future_window_is_unavailable():
|
||||
from datetime import timedelta
|
||||
|
||||
from tradingagents.dataflows.date_window import coverage_gap
|
||||
today = datetime.now(timezone.utc).date()
|
||||
today = datetime.now(UTC).date()
|
||||
out = coverage_gap([], str(today), str(today + timedelta(days=3)), "Feed", "items")
|
||||
assert out is not None and "past today" in out
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ as-of date. All three sources share dataflows.date_window.in_window.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -38,10 +38,10 @@ class _JsonResp:
|
||||
def test_in_window_bounds_and_exclusive_upper():
|
||||
start = datetime(2026, 5, 1)
|
||||
end = datetime(2026, 5, 9)
|
||||
assert in_window(datetime(2026, 5, 5, tzinfo=timezone.utc), start, end) is True
|
||||
assert in_window(datetime(2026, 5, 9, 23, 59, tzinfo=timezone.utc), start, end) is True
|
||||
assert in_window(datetime(2026, 5, 5, tzinfo=UTC), start, end) is True
|
||||
assert in_window(datetime(2026, 5, 9, 23, 59, tzinfo=UTC), start, end) is True
|
||||
# exactly midnight after end -> excluded (no leak)
|
||||
assert in_window(datetime(2026, 5, 10, 0, 0, tzinfo=timezone.utc), start, end) is False
|
||||
assert in_window(datetime(2026, 5, 10, 0, 0, tzinfo=UTC), start, end) is False
|
||||
# offset-aware converted, not truncated: 05-10T01:00+05:00 == 05-09T20:00Z
|
||||
assert in_window(datetime.fromisoformat("2026-05-10T01:00:00+05:00"), start, end) is True
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_in_window_bounds_and_exclusive_upper():
|
||||
def test_in_window_undated_excluded_in_backtest_kept_live():
|
||||
old = datetime(2026, 5, 9)
|
||||
assert in_window(None, datetime(2026, 5, 1), old) is False # historical
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
assert in_window(None, now, now) is True # live
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ def test_stocktwits_no_window_is_unfiltered(monkeypatch):
|
||||
# --- Reddit -----------------------------------------------------------------
|
||||
|
||||
def _epoch(date_str):
|
||||
return int(datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp())
|
||||
return int(datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC).timestamp())
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -184,7 +184,7 @@ def test_reddit_window_straddling_the_lookback_is_unavailable(monkeypatch):
|
||||
# Ten days ago through five days ago: the week-long search never reaches the
|
||||
# first three days, so an empty result cannot stand for the whole window.
|
||||
from datetime import timedelta
|
||||
today = datetime.now(timezone.utc).date()
|
||||
today = datetime.now(UTC).date()
|
||||
monkeypatch.setattr(reddit, "_fetch_subreddit_rss", lambda *a, **k: [])
|
||||
out = reddit.fetch_reddit_posts(
|
||||
"AAPL", subreddits=("stocks",),
|
||||
@@ -198,7 +198,7 @@ def test_reddit_standard_week_window_empty_is_a_real_absence(monkeypatch):
|
||||
# The graph's window is [trade_date - 7, trade_date]; the week-long search
|
||||
# covers it, so an empty result is genuine silence.
|
||||
from datetime import timedelta
|
||||
today = datetime.now(timezone.utc).date()
|
||||
today = datetime.now(UTC).date()
|
||||
monkeypatch.setattr(reddit, "_fetch_subreddit_rss", lambda *a, **k: [])
|
||||
out = reddit.fetch_reddit_posts(
|
||||
"AAPL", subreddits=("stocks",),
|
||||
@@ -212,7 +212,7 @@ def test_reddit_full_page_does_not_vouch_for_older_days(monkeypatch):
|
||||
# 100 posts from today say nothing about five days ago: the page may have
|
||||
# cut older matches off, so the window stays unavailable.
|
||||
from datetime import timedelta
|
||||
today = datetime.now(timezone.utc).date()
|
||||
today = datetime.now(UTC).date()
|
||||
ts = _epoch(str(today))
|
||||
page = [{"title": f"T{i}", "created_utc": ts, "subreddit": "stocks"} for i in range(reddit._FEED_PAGE)]
|
||||
monkeypatch.setattr(reddit, "_fetch_subreddit_rss", lambda *a, **k: page)
|
||||
|
||||
@@ -18,7 +18,7 @@ so that:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
@@ -63,7 +63,7 @@ def _coerce_optional_float(value):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PortfolioRating(str, Enum):
|
||||
class PortfolioRating(StrEnum):
|
||||
"""5-tier rating used by the Research Manager and Portfolio Manager."""
|
||||
|
||||
BUY = "Buy"
|
||||
@@ -73,7 +73,7 @@ class PortfolioRating(str, Enum):
|
||||
SELL = "Sell"
|
||||
|
||||
|
||||
class TraderAction(str, Enum):
|
||||
class TraderAction(StrEnum):
|
||||
"""3-tier transaction direction used by the Trader.
|
||||
|
||||
The Trader's job is to translate the Research Manager's investment plan
|
||||
@@ -293,7 +293,7 @@ def render_pm_decision(decision: PortfolioDecision) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SentimentBand(str, Enum):
|
||||
class SentimentBand(StrEnum):
|
||||
"""Discrete sentiment direction produced by the Sentiment Analyst.
|
||||
|
||||
Six tiers keep the signal granular enough to be actionable while remaining
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
|
||||
|
||||
def to_utc(dt: datetime) -> datetime:
|
||||
"""Normalize a datetime to UTC-aware; a naive value is assumed to be UTC."""
|
||||
return dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt.astimezone(timezone.utc)
|
||||
return dt.replace(tzinfo=UTC) if dt.tzinfo is None else dt.astimezone(UTC)
|
||||
|
||||
|
||||
def in_window(pub_dt: datetime | None, start_dt: datetime, end_dt: datetime) -> bool:
|
||||
@@ -28,7 +28,7 @@ def in_window(pub_dt: datetime | None, start_dt: datetime, end_dt: datetime) ->
|
||||
end = to_utc(end_dt)
|
||||
if pub_dt is not None:
|
||||
return to_utc(start_dt) <= to_utc(pub_dt) < end + timedelta(days=1)
|
||||
return end >= datetime.now(timezone.utc) - timedelta(days=1)
|
||||
return end >= datetime.now(UTC) - timedelta(days=1)
|
||||
|
||||
|
||||
def get_current_date() -> str:
|
||||
@@ -57,7 +57,7 @@ def coverage_gap(
|
||||
returned newest-first and unbroken in time; a merged or relevance-ranked
|
||||
result passes no dates, leaving only the present as the bound.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
oldest = min((to_utc(d) for d in dates if d is not None), default=now)
|
||||
if datetime.strptime(end_date, "%Y-%m-%d").date() > now.date():
|
||||
reason = "the window extends past today"
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ outcomes (a "Yes" at 0.76 means the market prices a 76% chance).
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import requests
|
||||
|
||||
@@ -101,7 +101,7 @@ def get_prediction_markets(topic: str, limit: int | None = None, as_of_date: str
|
||||
f"Proceed without prediction-market signal for '{topic}'."
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
candidates = [
|
||||
m
|
||||
for event in data.get("events", [])
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import re
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
@@ -51,7 +51,7 @@ def _within_window(posts, start_date, end_date):
|
||||
def _posted_at(post) -> datetime | None:
|
||||
"""A post's ``created_utc`` epoch as a UTC datetime, or None when missing."""
|
||||
ts = post.get("created_utc")
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc) if ts else None
|
||||
return datetime.fromtimestamp(ts, tz=UTC) if ts else None
|
||||
|
||||
|
||||
def _coverage_dates(posts) -> list:
|
||||
@@ -61,7 +61,7 @@ def _coverage_dates(posts) -> list:
|
||||
themselves do."""
|
||||
dates = [_posted_at(p) for p in posts]
|
||||
if len(posts) < _FEED_PAGE:
|
||||
dates.append(datetime.now(timezone.utc) - _SEARCH_LOOKBACK)
|
||||
dates.append(datetime.now(UTC) - _SEARCH_LOOKBACK)
|
||||
return dates
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"""yfinance-based news data fetching functions."""
|
||||
|
||||
import contextlib
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import yfinance as yf
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -47,7 +47,7 @@ def _extract_article_data(article: dict) -> dict:
|
||||
# Epoch seconds are UTC; parse them as UTC-aware so filtering does
|
||||
# not shift with the host timezone (#1126).
|
||||
with contextlib.suppress(ValueError, OSError, TypeError):
|
||||
pub_date = datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
pub_date = datetime.fromtimestamp(ts, tz=UTC)
|
||||
return {
|
||||
"title": article.get("title", "No title"),
|
||||
"summary": article.get("summary", ""),
|
||||
|
||||
Reference in New Issue
Block a user