docs: add filter OHLCV cache implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
883e7ea352
commit
c0b5353327
|
|
@ -0,0 +1,610 @@
|
||||||
|
# Filter OHLCV Cache Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace three per-ticker yfinance HTTP calls in the filter stage with OHLCV cache lookups, eliminating rate limit errors that drop candidates before they reach the ranker.
|
||||||
|
|
||||||
|
**Architecture:** Load `download_ohlcv_cached()` once at the start of `CandidateFilter.filter()` for the ~60 candidate tickers, pass the resulting dict into `_filter_and_enrich_candidates()`, and replace all three yfinance call sites (current price, intraday check, recent-move check) with cache lookups. The existing `get_stock_price()` call remains as a fallback for tickers missing from the cache.
|
||||||
|
|
||||||
|
**Tech Stack:** Python, pandas, `tradingagents/dataflows/data_cache/ohlcv_cache.py` (`download_ohlcv_cached`), `tradingagents/dataflows/discovery/filter.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `tradingagents/dataflows/discovery/filter.py` | Main changes: load cache in `filter()`, remove `_fetch_batch_prices()`, update `_filter_and_enrich_candidates()` signature and three call sites |
|
||||||
|
| `tests/test_filter_ohlcv_cache.py` | New test file |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Write failing tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/test_filter_ohlcv_cache.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the test file**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/test_filter_ohlcv_cache.py
|
||||||
|
"""Tests for OHLCV-cache-backed filter enrichment."""
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def _make_ohlcv(closes: list[float]) -> pd.DataFrame:
|
||||||
|
"""Build a minimal OHLCV DataFrame from a list of closing prices."""
|
||||||
|
dates = pd.date_range("2026-01-01", periods=len(closes), freq="B")
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"Open": closes,
|
||||||
|
"High": closes,
|
||||||
|
"Low": closes,
|
||||||
|
"Close": closes,
|
||||||
|
"Volume": [1_000_000] * len(closes),
|
||||||
|
},
|
||||||
|
index=dates,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_filter(config_overrides=None):
|
||||||
|
"""Instantiate a CandidateFilter with minimal config."""
|
||||||
|
from tradingagents.dataflows.discovery.filter import CandidateFilter
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"discovery": {
|
||||||
|
"ohlcv_cache_dir": "data/ohlcv_cache",
|
||||||
|
"filters": {
|
||||||
|
"min_average_volume": 0,
|
||||||
|
"volume_lookback_days": 10,
|
||||||
|
"filter_same_day_movers": True,
|
||||||
|
"intraday_movement_threshold": 10.0,
|
||||||
|
"filter_recent_movers": True,
|
||||||
|
"recent_movement_lookback_days": 7,
|
||||||
|
"recent_movement_threshold": 10.0,
|
||||||
|
"recent_mover_action": "filter",
|
||||||
|
"volume_cache_key": "default",
|
||||||
|
"min_market_cap": 0,
|
||||||
|
"compression_atr_pct_max": 2.0,
|
||||||
|
"compression_bb_width_max": 6.0,
|
||||||
|
"compression_min_volume_ratio": 1.3,
|
||||||
|
"filter_fundamental_risk": False,
|
||||||
|
"min_z_score": None,
|
||||||
|
"min_f_score": None,
|
||||||
|
},
|
||||||
|
"enrichment": {
|
||||||
|
"batch_news_vendor": "google",
|
||||||
|
"batch_news_batch_size": 150,
|
||||||
|
"news_lookback_days": 0.5,
|
||||||
|
"context_max_snippets": 2,
|
||||||
|
"context_snippet_max_chars": 140,
|
||||||
|
},
|
||||||
|
"max_candidates_to_analyze": 200,
|
||||||
|
"analyze_all_candidates": False,
|
||||||
|
"final_recommendations": 15,
|
||||||
|
"truncate_ranking_context": False,
|
||||||
|
"max_news_chars": 500,
|
||||||
|
"max_insider_chars": 300,
|
||||||
|
"max_recommendations_chars": 300,
|
||||||
|
"log_tool_calls": False,
|
||||||
|
"log_tool_calls_console": False,
|
||||||
|
"log_prompts_console": False,
|
||||||
|
"tool_log_max_chars": 10_000,
|
||||||
|
"tool_log_exclude": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config_overrides:
|
||||||
|
config["discovery"]["filters"].update(config_overrides)
|
||||||
|
return CandidateFilter(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_price_comes_from_ohlcv_cache():
|
||||||
|
"""current_price on the candidate should be the last close from the OHLCV cache."""
|
||||||
|
closes = [100.0] * 210 + [123.45] # last close = 123.45
|
||||||
|
ohlcv_data = {"AAPL": _make_ohlcv(closes)}
|
||||||
|
|
||||||
|
f = _make_filter()
|
||||||
|
candidate = {"ticker": "AAPL", "source": "test", "context": "test", "priority": "medium"}
|
||||||
|
|
||||||
|
price = f._price_from_cache("AAPL", ohlcv_data)
|
||||||
|
assert price == pytest.approx(123.45)
|
||||||
|
|
||||||
|
|
||||||
|
def test_intraday_check_from_cache_not_moved():
|
||||||
|
"""intraday check: <10% day-over-day change → already_moved=False."""
|
||||||
|
closes = [100.0] * 210 + [105.0] # +5% last day — under threshold
|
||||||
|
ohlcv_data = {"AAPL": _make_ohlcv(closes)}
|
||||||
|
|
||||||
|
f = _make_filter()
|
||||||
|
result = f._intraday_from_cache("AAPL", ohlcv_data, threshold=10.0)
|
||||||
|
assert result["already_moved"] is False
|
||||||
|
assert result["intraday_change_pct"] == pytest.approx(5.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_intraday_check_from_cache_moved():
|
||||||
|
"""intraday check: >10% day-over-day change → already_moved=True."""
|
||||||
|
closes = [100.0] * 210 + [115.0] # +15% last day — over threshold
|
||||||
|
ohlcv_data = {"AAPL": _make_ohlcv(closes)}
|
||||||
|
|
||||||
|
f = _make_filter()
|
||||||
|
result = f._intraday_from_cache("AAPL", ohlcv_data, threshold=10.0)
|
||||||
|
assert result["already_moved"] is True
|
||||||
|
assert result["intraday_change_pct"] == pytest.approx(15.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_move_check_from_cache_leading():
|
||||||
|
"""recent-move check: <10% change over 7 days → status=leading."""
|
||||||
|
closes = [100.0] * 205 + [103.0] * 7 # flat last 7 days
|
||||||
|
ohlcv_data = {"AAPL": _make_ohlcv(closes)}
|
||||||
|
|
||||||
|
f = _make_filter()
|
||||||
|
result = f._recent_move_from_cache("AAPL", ohlcv_data, lookback_days=7, threshold=10.0)
|
||||||
|
assert result["status"] == "leading"
|
||||||
|
assert abs(result["price_change_pct"]) < 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_move_check_from_cache_lagging():
|
||||||
|
"""recent-move check: >10% change over 7 days → status=lagging."""
|
||||||
|
closes = [100.0] * 205 + [100.0] * 6 + [115.0] # +15% in last day within window
|
||||||
|
ohlcv_data = {"AAPL": _make_ohlcv(closes)}
|
||||||
|
|
||||||
|
f = _make_filter()
|
||||||
|
result = f._recent_move_from_cache("AAPL", ohlcv_data, lookback_days=7, threshold=10.0)
|
||||||
|
assert result["status"] == "lagging"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_miss_returns_none():
|
||||||
|
"""If ticker is not in ohlcv_data, helper returns None."""
|
||||||
|
f = _make_filter()
|
||||||
|
assert f._price_from_cache("MISSING", {}) is None
|
||||||
|
assert f._intraday_from_cache("MISSING", {}, threshold=10.0) is None
|
||||||
|
assert f._recent_move_from_cache("MISSING", {}, lookback_days=7, threshold=10.0) is None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/youssefaitousarrah/Documents/TradingAgents
|
||||||
|
python -m pytest tests/test_filter_ohlcv_cache.py -v 2>&1 | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All 5 tests FAIL with `AttributeError: 'CandidateFilter' object has no attribute '_price_from_cache'`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add the three cache helper methods
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tradingagents/dataflows/discovery/filter.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add import at top of filter.py**
|
||||||
|
|
||||||
|
In `tradingagents/dataflows/discovery/filter.py`, add to the existing imports block (after line 6, `import pandas as pd`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from tradingagents.dataflows.data_cache.ohlcv_cache import download_ohlcv_cached
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the three helper methods to `CandidateFilter`**
|
||||||
|
|
||||||
|
Add these three methods to `CandidateFilter` class, just before the `_fetch_batch_volume` method (around line 301):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _price_from_cache(
|
||||||
|
self, ticker: str, ohlcv_data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Return last closing price from OHLCV cache, or None if ticker missing."""
|
||||||
|
df = ohlcv_data.get(ticker.upper())
|
||||||
|
if df is None or df.empty or "Close" not in df.columns:
|
||||||
|
return None
|
||||||
|
close = df["Close"].dropna()
|
||||||
|
if close.empty:
|
||||||
|
return None
|
||||||
|
return float(close.iloc[-1])
|
||||||
|
|
||||||
|
def _intraday_from_cache(
|
||||||
|
self, ticker: str, ohlcv_data: Dict[str, Any], threshold: float
|
||||||
|
) -> Any:
|
||||||
|
"""Compute day-over-day % change from last two daily closes.
|
||||||
|
|
||||||
|
Returns dict with 'already_moved' (bool) and 'intraday_change_pct' (float),
|
||||||
|
or None if ticker missing from cache or insufficient data.
|
||||||
|
"""
|
||||||
|
df = ohlcv_data.get(ticker.upper())
|
||||||
|
if df is None or df.empty or "Close" not in df.columns:
|
||||||
|
return None
|
||||||
|
close = df["Close"].dropna()
|
||||||
|
if len(close) < 2:
|
||||||
|
return None
|
||||||
|
prev_close = float(close.iloc[-2])
|
||||||
|
last_close = float(close.iloc[-1])
|
||||||
|
if prev_close <= 0:
|
||||||
|
return None
|
||||||
|
pct = (last_close - prev_close) / prev_close * 100
|
||||||
|
return {
|
||||||
|
"already_moved": pct > threshold,
|
||||||
|
"intraday_change_pct": round(pct, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _recent_move_from_cache(
|
||||||
|
self, ticker: str, ohlcv_data: Dict[str, Any], lookback_days: int, threshold: float
|
||||||
|
) -> Any:
|
||||||
|
"""Compute % change over last N daily closes.
|
||||||
|
|
||||||
|
Returns dict with 'status' ('leading'|'lagging') and 'price_change_pct' (float),
|
||||||
|
or None if ticker missing from cache or insufficient data.
|
||||||
|
"""
|
||||||
|
df = ohlcv_data.get(ticker.upper())
|
||||||
|
if df is None or df.empty or "Close" not in df.columns:
|
||||||
|
return None
|
||||||
|
close = df["Close"].dropna()
|
||||||
|
if len(close) < lookback_days + 1:
|
||||||
|
return None
|
||||||
|
price_start = float(close.iloc[-(lookback_days + 1)])
|
||||||
|
price_end = float(close.iloc[-1])
|
||||||
|
if price_start <= 0:
|
||||||
|
return None
|
||||||
|
pct = (price_end - price_start) / price_start * 100
|
||||||
|
reacted = abs(pct) >= threshold
|
||||||
|
return {
|
||||||
|
"status": "lagging" if reacted else "leading",
|
||||||
|
"price_change_pct": round(pct, 2),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests — should pass now**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_filter_ohlcv_cache.py -v 2>&1 | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All 5 tests PASS
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tradingagents/dataflows/discovery/filter.py tests/test_filter_ohlcv_cache.py
|
||||||
|
git commit -m "feat(filter): add OHLCV cache helper methods for price/intraday/recent-move"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Load OHLCV cache in `filter()` and wire into `_filter_and_enrich_candidates()`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tradingagents/dataflows/discovery/filter.py:151-188` (the `filter()` method)
|
||||||
|
- Modify: `tradingagents/dataflows/discovery/filter.py:409` (`_filter_and_enrich_candidates()` signature)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update `filter()` to load OHLCV cache and remove `_fetch_batch_prices` call**
|
||||||
|
|
||||||
|
Replace lines 172-188 in `filter()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Was:
|
||||||
|
volume_by_ticker = self._fetch_batch_volume(state, candidates)
|
||||||
|
news_by_ticker = self._fetch_batch_news(start_date, end_date, candidates)
|
||||||
|
price_by_ticker = self._fetch_batch_prices(candidates)
|
||||||
|
|
||||||
|
(
|
||||||
|
filtered_candidates,
|
||||||
|
filtered_reasons,
|
||||||
|
failed_tickers,
|
||||||
|
delisted_cache,
|
||||||
|
) = self._filter_and_enrich_candidates(
|
||||||
|
state=state,
|
||||||
|
candidates=candidates,
|
||||||
|
volume_by_ticker=volume_by_ticker,
|
||||||
|
news_by_ticker=news_by_ticker,
|
||||||
|
price_by_ticker=price_by_ticker,
|
||||||
|
end_date=end_date,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```python
|
||||||
|
volume_by_ticker = self._fetch_batch_volume(state, candidates)
|
||||||
|
news_by_ticker = self._fetch_batch_news(start_date, end_date, candidates)
|
||||||
|
|
||||||
|
# Load OHLCV cache for candidate tickers — replaces per-ticker yfinance calls
|
||||||
|
cache_dir = self.config.get("discovery", {}).get("ohlcv_cache_dir", "data/ohlcv_cache")
|
||||||
|
candidate_tickers = [c["ticker"].upper() for c in candidates if c.get("ticker")]
|
||||||
|
logger.info(f"Loading OHLCV cache for {len(candidate_tickers)} candidate tickers...")
|
||||||
|
ohlcv_data = download_ohlcv_cached(candidate_tickers, period="1y", cache_dir=cache_dir)
|
||||||
|
logger.info(f"OHLCV cache loaded for {len(ohlcv_data)}/{len(candidate_tickers)} tickers")
|
||||||
|
|
||||||
|
(
|
||||||
|
filtered_candidates,
|
||||||
|
filtered_reasons,
|
||||||
|
failed_tickers,
|
||||||
|
delisted_cache,
|
||||||
|
) = self._filter_and_enrich_candidates(
|
||||||
|
state=state,
|
||||||
|
candidates=candidates,
|
||||||
|
volume_by_ticker=volume_by_ticker,
|
||||||
|
news_by_ticker=news_by_ticker,
|
||||||
|
ohlcv_data=ohlcv_data,
|
||||||
|
end_date=end_date,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update `_filter_and_enrich_candidates()` signature**
|
||||||
|
|
||||||
|
Change the method signature at line ~409 from:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _filter_and_enrich_candidates(
|
||||||
|
self,
|
||||||
|
state: Dict[str, Any],
|
||||||
|
candidates: List[Dict[str, Any]],
|
||||||
|
volume_by_ticker: Dict[str, Any],
|
||||||
|
news_by_ticker: Dict[str, Any],
|
||||||
|
price_by_ticker: Dict[str, float],
|
||||||
|
end_date: str,
|
||||||
|
):
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _filter_and_enrich_candidates(
|
||||||
|
self,
|
||||||
|
state: Dict[str, Any],
|
||||||
|
candidates: List[Dict[str, Any]],
|
||||||
|
volume_by_ticker: Dict[str, Any],
|
||||||
|
news_by_ticker: Dict[str, Any],
|
||||||
|
ohlcv_data: Dict[str, Any],
|
||||||
|
end_date: str,
|
||||||
|
):
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run existing tests to make sure nothing is broken**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_filter_ohlcv_cache.py -v 2>&1 | tail -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All 5 PASS (the helper methods don't depend on the signature change)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tradingagents/dataflows/discovery/filter.py
|
||||||
|
git commit -m "feat(filter): load OHLCV cache in filter() and update _filter_and_enrich_candidates signature"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Replace the three yfinance call sites in `_filter_and_enrich_candidates`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tradingagents/dataflows/discovery/filter.py:435-534`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `check_intraday_movement` call site**
|
||||||
|
|
||||||
|
Find this block (around line 440):
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.filter_same_day_movers:
|
||||||
|
from tradingagents.dataflows.y_finance import check_intraday_movement
|
||||||
|
|
||||||
|
try:
|
||||||
|
intraday_check = check_intraday_movement(
|
||||||
|
ticker=ticker, movement_threshold=self.intraday_movement_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip if already moved significantly today
|
||||||
|
if intraday_check.get("already_moved"):
|
||||||
|
filtered_reasons["intraday_moved"] += 1
|
||||||
|
intraday_pct = intraday_check.get("intraday_change_pct", 0)
|
||||||
|
logger.info(
|
||||||
|
f"Filtered {ticker}: Already moved {intraday_pct:+.1f}% today (stale)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add intraday data to candidate metadata for ranking
|
||||||
|
cand["intraday_change_pct"] = intraday_check.get("intraday_change_pct", 0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Don't filter out if check fails, just log
|
||||||
|
logger.warning(f"Could not check intraday movement for {ticker}: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.filter_same_day_movers:
|
||||||
|
intraday_check = self._intraday_from_cache(
|
||||||
|
ticker, ohlcv_data, self.intraday_movement_threshold
|
||||||
|
)
|
||||||
|
if intraday_check is not None:
|
||||||
|
if intraday_check.get("already_moved"):
|
||||||
|
filtered_reasons["intraday_moved"] += 1
|
||||||
|
intraday_pct = intraday_check.get("intraday_change_pct", 0)
|
||||||
|
logger.info(
|
||||||
|
f"Filtered {ticker}: Already moved {intraday_pct:+.1f}% today (stale)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
cand["intraday_change_pct"] = intraday_check.get("intraday_change_pct", 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace `check_if_price_reacted` call site**
|
||||||
|
|
||||||
|
Find this block (around line 465):
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.filter_recent_movers:
|
||||||
|
from tradingagents.dataflows.y_finance import check_if_price_reacted
|
||||||
|
|
||||||
|
try:
|
||||||
|
reaction = check_if_price_reacted(
|
||||||
|
ticker=ticker,
|
||||||
|
lookback_days=self.recent_movement_lookback_days,
|
||||||
|
reaction_threshold=self.recent_movement_threshold,
|
||||||
|
)
|
||||||
|
cand["recent_change_pct"] = reaction.get("price_change_pct")
|
||||||
|
cand["recent_move_status"] = reaction.get("status")
|
||||||
|
|
||||||
|
if reaction.get("status") == "lagging":
|
||||||
|
if self.recent_mover_action == "filter":
|
||||||
|
filtered_reasons["recent_moved"] += 1
|
||||||
|
change_pct = reaction.get("price_change_pct", 0)
|
||||||
|
logger.info(
|
||||||
|
f"Filtered {ticker}: Already moved {change_pct:+.1f}% in last "
|
||||||
|
f"{self.recent_movement_lookback_days} days"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if self.recent_mover_action == "deprioritize":
|
||||||
|
cand["priority"] = "low"
|
||||||
|
existing_context = cand.get("context", "")
|
||||||
|
change_pct = reaction.get("price_change_pct", 0)
|
||||||
|
cand["context"] = (
|
||||||
|
f"{existing_context} | ⚠️ Recent move: {change_pct:+.1f}% "
|
||||||
|
f"over {self.recent_movement_lookback_days}d"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not check recent movement for {ticker}: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.filter_recent_movers:
|
||||||
|
reaction = self._recent_move_from_cache(
|
||||||
|
ticker,
|
||||||
|
ohlcv_data,
|
||||||
|
self.recent_movement_lookback_days,
|
||||||
|
self.recent_movement_threshold,
|
||||||
|
)
|
||||||
|
if reaction is not None:
|
||||||
|
cand["recent_change_pct"] = reaction.get("price_change_pct")
|
||||||
|
cand["recent_move_status"] = reaction.get("status")
|
||||||
|
|
||||||
|
if reaction.get("status") == "lagging":
|
||||||
|
if self.recent_mover_action == "filter":
|
||||||
|
filtered_reasons["recent_moved"] += 1
|
||||||
|
change_pct = reaction.get("price_change_pct", 0)
|
||||||
|
logger.info(
|
||||||
|
f"Filtered {ticker}: Already moved {change_pct:+.1f}% in last "
|
||||||
|
f"{self.recent_movement_lookback_days} days"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if self.recent_mover_action == "deprioritize":
|
||||||
|
cand["priority"] = "low"
|
||||||
|
existing_context = cand.get("context", "")
|
||||||
|
change_pct = reaction.get("price_change_pct", 0)
|
||||||
|
cand["context"] = (
|
||||||
|
f"{existing_context} | ⚠️ Recent move: {change_pct:+.1f}% "
|
||||||
|
f"over {self.recent_movement_lookback_days}d"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace `get_stock_price` / `price_by_ticker` call site**
|
||||||
|
|
||||||
|
Find this block (around line 520):
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
from tradingagents.dataflows.y_finance import get_fundamentals, get_stock_price
|
||||||
|
|
||||||
|
# Get current price — prefer batch result, fall back to per-ticker
|
||||||
|
current_price = price_by_ticker.get(ticker.upper())
|
||||||
|
if current_price is None:
|
||||||
|
current_price = get_stock_price(ticker)
|
||||||
|
cand["current_price"] = current_price
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
from tradingagents.dataflows.y_finance import get_fundamentals, get_stock_price
|
||||||
|
|
||||||
|
# Get current price — prefer OHLCV cache, fall back to per-ticker yfinance
|
||||||
|
current_price = self._price_from_cache(ticker, ohlcv_data)
|
||||||
|
if current_price is None:
|
||||||
|
current_price = get_stock_price(ticker)
|
||||||
|
cand["current_price"] = current_price
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run all filter tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_filter_ohlcv_cache.py -v 2>&1 | tail -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All 5 PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tradingagents/dataflows/discovery/filter.py
|
||||||
|
git commit -m "feat(filter): replace per-ticker yfinance calls with OHLCV cache lookups"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Remove `_fetch_batch_prices` method
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tradingagents/dataflows/discovery/filter.py:357-407`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Delete `_fetch_batch_prices`**
|
||||||
|
|
||||||
|
Remove the entire method from line 357 to 407:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _fetch_batch_prices(self, candidates: List[Dict[str, Any]]) -> Dict[str, float]:
|
||||||
|
"""Batch-fetch current prices for all candidates in one request.
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
# ... entire method body
|
||||||
|
```
|
||||||
|
|
||||||
|
The method is no longer called from anywhere after Task 3.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify tests still pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_filter_ohlcv_cache.py -v 2>&1 | tail -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All 5 PASS
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tradingagents/dataflows/discovery/filter.py
|
||||||
|
git commit -m "refactor(filter): remove _fetch_batch_prices (replaced by OHLCV cache)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Push and trigger a discovery run to validate
|
||||||
|
|
||||||
|
- [ ] **Step 1: Push all commits**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Trigger a discovery run**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run "Daily Discovery" --repo Aitous/TradingAgents --ref main
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Monitor and verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh run list --repo Aitous/TradingAgents --workflow "Daily Discovery" --limit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch for:
|
||||||
|
- Zero `"Too Many Requests"` errors in the filter stage
|
||||||
|
- `"OHLCV cache loaded for X/Y tickers"` log line showing high hit rate (expect >95%)
|
||||||
|
- `"Starting candidates: N"` in filter summary — should be close to the full scan count (~60), not 11
|
||||||
|
- Ranker receiving more than 11 candidates
|
||||||
|
- Final recommendations > 2 picks
|
||||||
Loading…
Reference in New Issue