Summary
#1300 was fixed by withholding the vendor profile on past dates, which is correct: Ticker.info and OVERVIEW carry today's market cap and ratios. A side effect is that every backtest now runs the Fundamentals Analyst with statements and no price attached to them. No market cap, no enterprise value, no multiple of any kind. run_backtest (#1234) is therefore scoring decisions made without knowing whether the stock was cheap or expensive.
sec_edgar.py already has what is needed to fill the gap without reopening the leak: every fact carries its filing date. A share count and trailing figures on file by curr_date, priced at that day's close, give the multiples as they stood.
Measured
Both arms ran on v0.5.0 (2d17df8) plus the one commit that adds the function; only the second arm routes to it. Runs executed two at a time in one process, each with its own data config; a first attempt without that isolation is how #1369 was found, and those runs were discarded.
Matched pair, 12 cells, one run per cell per arm: AAPL, NVDA, INTC, PFE on 2025-11-14, 2026-02-13, 2026-05-15. Market and Fundamentals analysts, one debate round, same models in both arms (served model names confirmed from the API: deepseek-flash, deepseek-v4-pro). Both arms read statements from sec_edgar,yfinance. Only difference: the second arm also routes get_fundamentals to the branch below. Counts are over the text of all nine agents' reports.
| Measure, 12 runs per arm |
Statements only |
+ valuation as of the date |
| Statements that valuation is missing, withheld or unavailable |
55 |
1 |
| Valuation multiples cited with a number |
35 |
447 |
| of which equal to the run date's P/E, P/S or P/B (5%) |
24 |
327 |
| Multiples cited by the research or portfolio manager |
5 (in 2 runs) |
53 (in 9 runs) |
| LLM cost, USD |
0.548 |
0.523 |
| Mean run time, minutes |
6.2 |
5.6 |
One caveat on the build under test: it also served enterprise value, EV/EBITDA and EV/Sales, which the proposed branch leaves out (see below). Matches are therefore counted against P/E, P/S and P/B only, and citations of the EV rows count as cited but unmatched.
Fewer missing-valuation statements in 12 of 12 cells, exact sign test p = 0.0005. That one remaining statement is accurate: the PFE analyst notes that forward estimates are unavailable, which the report states by design.
What the statements-only arm does instead: a few agents divide the close by diluted EPS by hand, some build a forward P/E on an EPS figure of their own, and none has market cap, P/S, P/B or a free cash flow yield. Everything downstream reads the fundamentals report, and it cites 2 multiples across 12 runs against 185.
Ratings are not claimed as evidence. They were 4 Overweight, 1 Hold, 7 Underweight against 4, 1, 6 and 1 Sell, with 4 of 12 cells changing, and one run per cell cannot separate that from run-to-run noise.
Counting rules, the per-cell table and the script are available on request. Two rounds of reading the hits by hand removed false matches (a "multiple without regulatory risk" is a multiple being used; dividend cover and net debt to EBITDA are not valuation multiples).
Proposed shape
Branch: davidalmeida90:feat/edgar-valuation, stacked on the capex fix in #1370 (diff against main). Offered as a reference; happy to open it as a pull request if that is useful. One function in the existing module, no new module, no config key, no account or API key, default behaviour unchanged: the vendor is served only when a user puts sec_edgar in the chain.
Vintage rules, stated in the output as well:
- Figures:
_as_of is reused unchanged, so every input is the latest filing on or before curr_date.
- Trailing twelve months: the fiscal year right after an annual report; later, fiscal year plus year to date minus the same stretch a year before. Three filed figures, each printed in the row (
Net Income (TTM): 99280 = fiscal year to 2024-09-28 (93736) + 9 months to 2025-06-28 (84544) - 9 months to 2024-06-29 (79000)). A missing part gives unavailable. A second quarter reported alone is never added as if it were a half year. This is consistent with the module's rule that a fourth quarter is never derived: nothing is estimated, and every part has a filing date.
- Shares: cover-page count (
dei:EntityCommonStockSharesOutstanding), then the balance sheet count, then the diluted average, first one counted within 200 days. Multi-class filers such as Alphabet have no cover-page total and fall through to the second.
- Price: last close on or before the date with
auto_adjust=False, and later splits undone, so it is the price that traded. A split between the share count and the date is applied to the count. Checked on NVDA either side of 2024-06-10 (3.01T and 3.22T) and TSLA after 2022-08-25.
- Book value: the newest stockholders' equity on file.
- Enterprise value is left out on purpose. A first version served EV, EV/EBITDA and EV/Sales. Debt is tagged too differently from one filer to the next to net it reliably, leases were ignored, and Alphabet, Tesla and Microsoft tag depreciation without amortization, so EBITDA ran low for them. A multiple that is wrong for some companies with nothing in the row to show it is worse than no multiple. It can return as its own step once debt has a rule that holds across filers.
- Stale guard: inputs older than 200 days give
unavailable, and a company with no recent share count raises NoMarketDataError, so the chain moves on.
- Not served: name, sector, beta, dividend yield and forward estimates have no filed vintage. Its header says so.
15 unit tests in the style of test_sec_edgar.py, synthetic filer, no network. Ruff clean.
Open questions for the maintainer
Price source. The close comes from yfinance inside the EDGAR module, whatever vendor the user configured for prices. It has to be an unadjusted close plus the split history, which get_stock_data does not expose today. If the chain should decide this too, the clean route is a small vendor-routed helper for an as-traded close.
Live dates. On a live date the function still serves, because the configured chain is the chain. A user with sec_edgar,yfinance then gets valuation from filings and loses the profile's name, sector and 52-week range on live runs. Deferring to the next vendor when curr_date is today is a two-line change if that reads better.
Summary
#1300 was fixed by withholding the vendor profile on past dates, which is correct:
Ticker.infoandOVERVIEWcarry today's market cap and ratios. A side effect is that every backtest now runs the Fundamentals Analyst with statements and no price attached to them. No market cap, no enterprise value, no multiple of any kind.run_backtest(#1234) is therefore scoring decisions made without knowing whether the stock was cheap or expensive.sec_edgar.pyalready has what is needed to fill the gap without reopening the leak: every fact carries its filing date. A share count and trailing figures on file bycurr_date, priced at that day's close, give the multiples as they stood.Measured
Both arms ran on v0.5.0 (2d17df8) plus the one commit that adds the function; only the second arm routes to it. Runs executed two at a time in one process, each with its own data config; a first attempt without that isolation is how #1369 was found, and those runs were discarded.
Matched pair, 12 cells, one run per cell per arm: AAPL, NVDA, INTC, PFE on 2025-11-14, 2026-02-13, 2026-05-15. Market and Fundamentals analysts, one debate round, same models in both arms (served model names confirmed from the API:
deepseek-flash,deepseek-v4-pro). Both arms read statements fromsec_edgar,yfinance. Only difference: the second arm also routesget_fundamentalsto the branch below. Counts are over the text of all nine agents' reports.One caveat on the build under test: it also served enterprise value, EV/EBITDA and EV/Sales, which the proposed branch leaves out (see below). Matches are therefore counted against P/E, P/S and P/B only, and citations of the EV rows count as cited but unmatched.
Fewer missing-valuation statements in 12 of 12 cells, exact sign test p = 0.0005. That one remaining statement is accurate: the PFE analyst notes that forward estimates are unavailable, which the report states by design.
What the statements-only arm does instead: a few agents divide the close by diluted EPS by hand, some build a forward P/E on an EPS figure of their own, and none has market cap, P/S, P/B or a free cash flow yield. Everything downstream reads the fundamentals report, and it cites 2 multiples across 12 runs against 185.
Ratings are not claimed as evidence. They were 4 Overweight, 1 Hold, 7 Underweight against 4, 1, 6 and 1 Sell, with 4 of 12 cells changing, and one run per cell cannot separate that from run-to-run noise.
Counting rules, the per-cell table and the script are available on request. Two rounds of reading the hits by hand removed false matches (a "multiple without regulatory risk" is a multiple being used; dividend cover and net debt to EBITDA are not valuation multiples).
Proposed shape
Branch:
davidalmeida90:feat/edgar-valuation, stacked on the capex fix in #1370 (diff against main). Offered as a reference; happy to open it as a pull request if that is useful. One function in the existing module, no new module, no config key, no account or API key, default behaviour unchanged: the vendor is served only when a user putssec_edgarin the chain.Vintage rules, stated in the output as well:
_as_ofis reused unchanged, so every input is the latest filing on or beforecurr_date.Net Income (TTM): 99280 = fiscal year to 2024-09-28 (93736) + 9 months to 2025-06-28 (84544) - 9 months to 2024-06-29 (79000)). A missing part givesunavailable. A second quarter reported alone is never added as if it were a half year. This is consistent with the module's rule that a fourth quarter is never derived: nothing is estimated, and every part has a filing date.dei:EntityCommonStockSharesOutstanding), then the balance sheet count, then the diluted average, first one counted within 200 days. Multi-class filers such as Alphabet have no cover-page total and fall through to the second.auto_adjust=False, and later splits undone, so it is the price that traded. A split between the share count and the date is applied to the count. Checked on NVDA either side of 2024-06-10 (3.01T and 3.22T) and TSLA after 2022-08-25.unavailable, and a company with no recent share count raisesNoMarketDataError, so the chain moves on.15 unit tests in the style of
test_sec_edgar.py, synthetic filer, no network. Ruff clean.Open questions for the maintainer
Price source. The close comes from yfinance inside the EDGAR module, whatever vendor the user configured for prices. It has to be an unadjusted close plus the split history, which
get_stock_datadoes not expose today. If the chain should decide this too, the clean route is a small vendor-routed helper for an as-traded close.Live dates. On a live date the function still serves, because the configured chain is the chain. A user with
sec_edgar,yfinancethen gets valuation from filings and loses the profile's name, sector and 52-week range on live runs. Deferring to the next vendor whencurr_dateis today is a two-line change if that reads better.