Commit Graph

106 Commits

Author SHA1 Message Date
Youssef Aitousarrah ece89d27ff hypothesis(minervini): add min_pct_from_high floor of 7% to avoid resistance stall 2026-04-16 15:36:39 -07:00
Youssef Aitousarrah 7d12a7abb3 fix(dark_pool_flow): default thresholds to 0 — Meridian pre-filters, don't double-gate 2026-04-16 12:56:31 -07:00
Youssef Aitousarrah 74091582f6 feat(scanner): add dark_pool_flow scanner — scrapes meridianfin.io FINRA Z-scored anomalies 2026-04-16 12:55:03 -07:00
Youssef Aitousarrah 6e43c7164a fix(analytics): merge recommendations into existing dated file instead of overwriting
Multiple runs on the same day (scheduled discovery, hypothesis runner, manual
re-runs) were each clobbering the shared YYYY-MM-DD.json file. Now merges by
loading existing picks and upserting new ones by ticker — later run wins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:04:52 -07:00
Youssef Aitousarrah 54583ee667 fix(filter): load OHLCV from full universe cache key to get cache hit
Requesting only candidate tickers produced a different cache key from the
nightly prefetch, triggering a fresh yfinance download that hit rate limits
(4/75 tickers returned). Now loads the full universe (same key as nightly
prefetch) and slices to candidate tickers — always a cache hit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 20:02:39 -07:00
Youssef Aitousarrah 9112b14177 fix(filter): deprioritize recent movers instead of dropping, exempt mean_reversion pipeline
- Change recent_mover_action default from "filter" to "deprioritize" so candidates
  that moved >10% in the past 7 days reach the ranker (with lowered priority + context
  annotation) rather than being silently dropped
- Exempt mean_reversion pipeline candidates (rsi_oversold) from the recent-mover action
  entirely — stocks that pulled back hard are the signal, not a disqualifier
- Hoist SCANNER_REGISTRY import to module level (removes duplicate inline imports)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:28:26 -07:00
Youssef Aitousarrah 471030facc fix(filter): deprioritize recent movers instead of dropping, exempt mean_reversion pipeline
- Change recent_mover_action default from "filter" to "deprioritize" so candidates
  that moved >10% in the past 7 days reach the ranker (with lowered priority + context
  annotation) rather than being silently dropped
- Exempt mean_reversion pipeline candidates (rsi_oversold) from the recent-mover action
  entirely — stocks that pulled back hard are the signal, not a disqualifier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:28:26 -07:00
Youssef Aitousarrah 01ea2f1dc4 refactor(filter): remove _fetch_batch_prices (replaced by OHLCV cache) 2026-04-15 12:53:12 -07:00
Youssef Aitousarrah 84438adf64 fix(filter): restore missing download_ohlcv_cached import 2026-04-15 12:52:20 -07:00
Youssef Aitousarrah c3ccfd5500 feat(filter): wire OHLCV cache into filter stage, replace yfinance call sites 2026-04-15 12:50:52 -07:00
Youssef Aitousarrah 7d48ad67bc fix(filter): add missing ohlcv import and intraday length guard 2026-04-15 12:46:33 -07:00
Youssef Aitousarrah d7e1b93509 feat(filter): add OHLCV cache helper methods for price/intraday/recent-move
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 12:45:04 -07:00
Youssef Aitousarrah 20d3e9fbbe feat(discovery): add mean_reversion pipeline for RSI oversold scanner
The rsi_oversold scanner is a contrarian signal, not a momentum signal —
it fires on short-term panic pullbacks within uptrends and expects a 3-7 day
bounce. Keeping it in the momentum pipeline conflates two fundamentally
different signal types and could cause the ranker to misprice it.

Changes:
- Add mean_reversion pipeline (priority 6, deep_dive_budget 5) to default_config.py
- Move rsi_oversold scanner from pipeline="momentum" to pipeline="mean_reversion"
  in both default_config.py and rsi_oversold.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 22:56:05 -07:00
Aitous 5c56f8dc26
research(autonomous): 2026-04-15 — automated research run (#19)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-14 22:49:43 -07:00
Youssef Aitousarrah 12a398a29a feat(scanners): remove caps for technical_breakout; add missing config entry
- technical_breakout: max_tickers 150 → 0 (reads from OHLCV cache, no need for cap)
- minervini: fix stale default max_tickers 50 → 0 in code (config already 0)
- default_config: add missing technical_breakout scanner config entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:19:54 -07:00
Youssef Aitousarrah 7567a17c82 feat(scanners): remove max_tickers caps and raise timeout now that cache is in place
- timeout_seconds: 30 → 120 (cache load alone takes ~28s for 1003 tickers)
- minervini max_tickers: 50 → 0 (no cap)
- high_52w_breakout max_tickers: 150 → 0 (no cap)
- Fix high_52w_breakout to treat max_tickers=0 as no cap (was slicing unconditionally)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:15:08 -07:00
Youssef Aitousarrah 6c1ba91e52 fix(universe): remove Liberty/Fox tickers from remap (yfinance uses compact form)
FOXA, NWSA, FWONA, LBTYA/K, LBRDA/K, LLYVA, GLIBA work fine without hyphens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:02:15 -07:00
Youssef Aitousarrah 43cc7eb623 fix(universe): robust iShares→yfinance ticker mapping for class shares
- Filter to Asset Class == Equity only (removes UBFUT, XTSLA non-equity rows)
- Static remap for 16 known iShares compact tickers → canonical yfinance hyphenated form
  (BRKB→BRK-B, BFA→BF-A, HEIA→HEI-A, LENB→LEN-B, UHALB→UHAL-B, CWENA→CWEN-A, etc.)
- Avoids false positives: META, NVDA, TSLA, ABNB, ZBRA, CMCSA preserved correctly
- Fix C401 in ohlcv_cache.py (set comprehension)
- Result: 1003 clean equity tickers from Russell 1000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:55:22 -07:00
Youssef Aitousarrah 0cee7b0161 fix(universe): robust iShares→yfinance ticker mapping for class shares
- Filter to Asset Class == Equity only (removes UBFUT cash collateral, XTSLA money market)
- Static remap for 16 known iShares compact tickers → canonical yfinance hyphenated form
  (BRKB→BRK-B, BFA→BF-A, HEIA→HEI-A, LENB→LEN-B, UHALB→UHAL-B, CWENA→CWEN-A, etc.)
- Avoids false positives: META, NVDA, TSLA, ABNB, ZBRA, CMCSA now preserved correctly
- Result: 1003 clean equity tickers from Russell 1000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:55:22 -07:00
Youssef Aitousarrah 59eb1eecee fix(minervini): remove undefined ticker reference in except clause
F821: _check_minervini_df no longer receives ticker as param after refactor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:41:17 -07:00
Youssef Aitousarrah 8050d6764f fix(cache): track ohlcv_cache.py and data_cache files in git
These were untracked so CI couldn't import tradingagents.dataflows.data_cache.ohlcv_cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:41:17 -07:00
Aitous 893eb05349
Merge pull request #18 from Aitous/research/current
research: new strategy findings — 2026-04-14
2026-04-14 16:39:47 -07:00
Youssef Aitousarrah 65cd0bb094 fix(universe): add missing __init__.py for data_cache; switch to Russell 1000 via iShares
- tradingagents/dataflows/data_cache/__init__.py: fixes ModuleNotFoundError in CI
- universe.py: fetches Russell 1000 from iShares IWB CSV with weekly disk cache + fallback
- default_config.py: universe_source = 'russell1000'
- data/universe_cache.json: initial cache (weekly TTL, auto-refreshed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:36:17 -07:00
Youssef Aitousarrah f87197ef41 style: apply black/ruff formatting fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:21:13 -07:00
Youssef Aitousarrah e15e2df7a5 feat(cache): unified ticker universe + nightly OHLCV prefetch
- tradingagents/dataflows/universe.py: single source of truth for ticker
  universe; all scanners now call load_universe(config) instead of
  duplicating the 3-level fallback chain with hardcoded "data/tickers.txt"

- scripts/prefetch_ohlcv.py: nightly script using existing ohlcv_cache.py
  incremental logic; first run downloads 1y history, subsequent runs append
  only new trading days

- .github/workflows/prefetch.yml: runs at 01:00 UTC daily, before all other
  workflows; commits updated parquet to repo

- Updated 6 scanners: minervini, high_52w_breakout, ml_signal, options_flow,
  sector_rotation, technical_breakout — removed duplicate DEFAULT_TICKER_FILE
  constants and _load_tickers_from_file() functions

- minervini, high_52w_breakout, technical_breakout: replace yf.download()
  with download_ohlcv_cached() — reads from prefetched cache instead of
  hitting yfinance at discovery time

- default_config.py: added discovery.ohlcv_cache_dir config key

- data/ohlcv_cache/: initial 1y backfill (588 tickers, 5.4MB parquet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:18:52 -07:00
github-actions[bot] 1dd00e467f research(autonomous): 2026-04-14 — automated research run 2026-04-14 21:17:06 +00:00
github-actions[bot] 17e77f036f research(autonomous): 2026-04-14 — automated research run 2026-04-14 13:47:21 -07:00
Youssef Aitousarrah 37029e554e fix(config): add missing earnings_beat and high_52w_breakout scanner configs
Both scanners were implemented by research PRs but never added to default_config.
Without entries they cannot be disabled or tuned from config and are invisible
to the settings UI. Also updated /research-strategy to make config entry mandatory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:47:21 -07:00
github-actions[bot] f862e91870 learn(iterate): 2026-04-14 — automated iteration run 2026-04-14 07:25:30 +00:00
Aitous 79a58a540c
Merge pull request #14 from Aitous/iterate/current
learn(iterate): automated improvements — 2026-04-13
2026-04-13 12:06:56 -07:00
github-actions[bot] 48a1c1672f research(autonomous): 2026-04-13 — automated research run 2026-04-13 09:06:49 +00:00
github-actions[bot] 17e45df41a learn(iterate): 2026-04-13 — automated iteration run 2026-04-13 07:52:59 +00:00
Youssef Aitousarrah f73681cf1c research(short-squeeze): 2026-04-12 — new short_squeeze scanner; high SI (>20%) as squeeze-risk discovery for cross-scanner confluence
Implements ShortSqueezeScanner wrapping existing get_short_interest() in finviz_scraper.py.
Research finding: raw high SI predicts negative long-term returns (academic); edge is using
SI as a squeeze-risk flag when combined with earnings_calendar or options_flow catalysts.
Directly addresses earnings_calendar pending hypothesis (APLD 30.6% SI was strongest setup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 19:10:36 -07:00
Youssef Aitousarrah a51d6193f8 research(short-squeeze): 2026-04-12 — new short_squeeze scanner; high SI (>20%) as squeeze-risk discovery for cross-scanner confluence
Implements ShortSqueezeScanner wrapping existing get_short_interest() in finviz_scraper.py.
Research finding: raw high SI predicts negative long-term returns (academic); edge is using
SI as a squeeze-risk flag when combined with earnings_calendar or options_flow catalysts.
Directly addresses earnings_calendar pending hypothesis (APLD 30.6% SI was strongest setup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 19:10:30 -07:00
Youssef Aitousarrah 612366fa45 learn(iterate): 2026-04-12 — document social_dd/early_accumulation; split social_dd from social_hype in ranker (55% 30d win rate vs 14.3%)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 19:03:03 -07:00
Youssef Aitousarrah 2d8b91b709 learn(iterate): 2026-04-12 — surface worst-performing strategies in ranker context; LLM now sees news_catalyst (0% 7d win rate) and social_hype (14.3%) as explicit penalties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 18:04:46 -07:00
Youssef Aitousarrah 7ec0e52b98 learn(iterate): 2026-04-12 — raise score threshold 55→65; minervini leads; insider_buying staleness pattern identified
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 18:04:46 -07:00
Youssef Aitousarrah 7585da3ac6 chore: sync local modifications
- active.json: updated days_elapsed from hypothesis runner
- hypotheses.py: black formatting applied by pre-commit hook
- .gitignore: local additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 18:04:38 -07:00
github-actions[bot] f17b2e4e02 learn(iterate): 2026-04-11 — automated iteration run 2026-04-11 06:55:28 +00:00
Youssef Aitousarrah 5b87a56f31 feat(hypotheses): add Hypotheses dashboard tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:52:58 -07:00
Youssef Aitousarrah c09cc7ec25 fix(y_finance): make suppress_yfinance_warnings thread-safe
The previous implementation redirected sys.stderr to /dev/null using a
context manager. This is not thread-safe: 8 concurrent scanner threads each
mutate sys.stderr, and when one thread's context manager closes the devnull
file, another thread that captured devnull as its saved stderr attempts to
write to the closed fd and raises "I/O operation on closed file".

This corrupted sys.stderr state caused _fetch_batch_prices to fail and
all per-ticker get_stock_price fallback calls to return None, resulting in
every candidate being dropped with "no data available".

Fix by suppressing at the Python logging level instead of redirecting
sys.stderr. Logger.setLevel() is protected by internal locks and is safe
to call from concurrent threads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:37:03 -07:00
Youssef Aitousarrah 704af1a855 fix(discovery): commit risk_metrics.py and reduce Minervini max_tickers to 50
Two bugs causing zero recommendations:

1. risk_metrics.py was untracked — importing it raised ModuleNotFoundError which
   was caught by the outer try/except in filter.py, silently dropping all 32
   candidates that reached the fundamental risk check stage.

2. Minervini scanner at max_tickers=200 took >5 min to download 200 tickers x 1y
   of OHLCV data. ThreadPoolExecutor.cancel() cannot kill a running thread, so the
   download kept running as a zombie thread for 20 more minutes after the pipeline
   completed, holding the Python process alive until the 30-min workflow timeout
   killed the entire job.

   Reducing to 50 tickers brings the download to ~75s, well under the 300s global
   scanner timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:24:29 -07:00
Youssef Aitousarrah 2e79c2245f fix(filter): only filter upside intraday movers, not downside
The same-day mover filter used abs() to check intraday movement, which
filtered both gap-ups AND gap-downs. On volatile/crash days (e.g. 2026-04-07)
all stocks dropped >10% from open, causing every candidate to be filtered and
leaving zero recommendations.

The filter's purpose is to avoid chasing stocks that already ran up. A stock
down 20% intraday is not "stale" — it should be evaluated on its merits.
Changed threshold check from abs(pct) >= threshold to pct >= threshold so only
upside movers are filtered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:51:41 -07:00
Youssef Aitousarrah 957b009da1 fix(minervini): cap ticker universe to prevent CI timeout
yf.download(592 tickers, period=1y) takes 20+ minutes in CI, causing
the 30-minute job timeout to trigger. Add max_tickers=200 (configurable)
to limit the batch download to the first N tickers from the file. The
concurrent scanner pool already has a 5-min global timeout, but the hung
download thread monopolises network connections and starves the filter stage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:24:22 -07:00
Youssef Aitousarrah b68a43ec0d feat(scanners): add minervini scanner to registry
minervini.py existed but was never committed. Without the file on the
remote, the __init__.py import added in the previous fix causes an
ImportError in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:51:42 -07:00
Youssef Aitousarrah 32d89c3bfc fix(ci): restore daily discovery workflow
- Add permissions: contents: write so git push works (was failing with 403)
- Add continue-on-error: true on discovery step so partial output still commits
- Change all commit/tracking/position steps to if: always() so they run regardless of discovery outcome
- Use commit-then-pull-rebase-then-push pattern to handle branch divergence
- Fix minervini scanner missing from scanners/__init__.py (enabled in config but never loaded)
- Fix .gitignore: results/* + !results/discovery/ so CI run logs can be committed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:46:21 -07:00
Youssef Aitousarrah 719a2d3f4e fix(scanners): rank by signal quality before limiting in 3 more scanners
Same issue as options_flow: early exit on candidate count discards strong
signals that happen to be later in iteration order.

insider_buying: Dict iteration order matched OpenInsider HTML scrape order,
not signal quality. Now scores by cluster buys + C-suite + dollar value,
then takes top N.

technical_breakout: Stopped at limit*2 in file order despite data already
being batch-downloaded (zero API cost to check all). Removed early exit,
scan full universe, sort by volume_multiple.

sector_rotation: Checked laggards in arbitrary dict order, spending API
calls on random tickers. Now sorts by most-negative 5d return first so
the strongest laggard candidates are checked before hitting the budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:46:21 -07:00
Youssef Aitousarrah 136fa47645 fix(options_flow): scan full universe before applying limit, rank by signal strength
Previously the scanner stopped as soon as self.limit candidates were found
from as_completed() futures. Since futures complete in non-deterministic
network-latency order, this was equivalent to random sampling — fast-to-
respond tickers won regardless of how strong their options signal was.

Fix: collect all candidates from the full universe, then sort by options_score
(unusual strike count weighted 1.5x for calls to favor bullish flow) before
applying the limit. The top-N strongest signals are now always returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:46:21 -07:00
Youssef Aitousarrah 61b731ac28 fix(filter): replace tqdm with logger in batch news functions to fix I/O error
tqdm writes to stderr immediately on __enter__, before any loop iteration.
In Streamlit's thread/subprocess context stderr can be a closed pipe, causing
'I/O operation on closed file' which _run_call catches and returns {} — so
the entire news enrichment step was silently skipped every run.

Replaced tqdm progress bars with logger.info() calls in:
- get_batch_stock_news_google() in openai.py
- get_batch_stock_news_openai() in openai.py
- Reddit DD parallel evaluation in reddit_api.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:46:21 -07:00
Youssef Aitousarrah c792b17ab6 fix(discovery): fix three scanner hang/validation bugs found in ranker_debug.log
1. executor.shutdown(wait=True) still blocked after global timeout (critical)
   The previous fix added timeout= to as_completed() but used `with
   ThreadPoolExecutor() as executor`, whose __exit__ calls shutdown(wait=True).
   This meant the process still hung waiting for stuck threads (ml_signal) even
   after the TimeoutError was caught.  Fixed by creating the executor explicitly
   and calling shutdown(wait=False) in a finally block.

2. ml_signal hangs on every run — "Batch-downloading 592 tickers (1y)..." never
   completes. Root cause: a single yfinance request for 592 tickers × 1 year of
   daily OHLCV is a very large payload that regularly times out at the network
   layer. Fixed by:
   - Reducing default lookback from "1y" to "6mo" (halves download size)
   - Splitting downloads into 150-ticker chunks so a slow chunk doesn't kill
     the whole scan (partial results are still returned)

3. C (Citigroup) and other single-letter NYSE tickers rejected as invalid.
   validate_ticker_format used ^[A-Z]{2,5}$ requiring at least 2 letters.
   Real tickers like C, A, F, T, X, M are 1 letter. Fixed to ^[A-Z]{1,5}$.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 22:35:42 -08:00