ohmydata 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ohmydata-0.2.1 → ohmydata-0.2.2}/CHANGELOG.md +12 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/PKG-INFO +65 -3
- {ohmydata-0.2.1 → ohmydata-0.2.2}/README.md +64 -2
- {ohmydata-0.2.1 → ohmydata-0.2.2}/pyproject.toml +1 -1
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/__init__.py +1 -1
- ohmydata-0.2.2/src/ohmydata/providers/yfinance/_fundamentals_parsers.py +199 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/fundamentals.py +93 -139
- {ohmydata-0.2.1 → ohmydata-0.2.2}/uv.lock +1 -1
- {ohmydata-0.2.1 → ohmydata-0.2.2}/.gitignore +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/adapters/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/adapters/polars.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/cli.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/_vintage_lock.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/availability.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/errors.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/facts.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/policy.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/provenance.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/rate_limit.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/snapshot.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/specs.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/core/vintage.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/artifacts.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/batch.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/cli.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/core_dataset.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/edgar.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/edgartools_adapter.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/endpoints.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/errors.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/financials.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/financials_dataset.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/http.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/nport.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/qualification.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/sec/qualification_dataset.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/client.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/endpoints.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/errors.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/observations/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/observations/capture.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/observations/serialization.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/etf_adjusted_bars.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/etf_index_mapping.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/etf_pcf_history.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/index_weight_vintage.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/lookthrough_bundle.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/shared.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/weighted_dividend_yield.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/vintage_artifacts.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/vintage_canonical.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/vintage_gates.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/vintage_plane.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/vintage_producer.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/client.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/endpoints.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/errors.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/yfinance/quality.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/tools/__init__.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/tools/cli.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/tools/drift_audit.py +0 -0
- {ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/tools/universes.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-09-06
|
|
4
|
+
|
|
5
|
+
- Enhanced `yfinance` fundamentals pipeline with institutional Forward P/E calibration and GAAP distortion detection:
|
|
6
|
+
- **Institutional FY1 Forward P/E Calibration**:
|
|
7
|
+
- Calibrated `forward_pe` anchored to current fiscal year (`0y`) sell-side consensus instead of out-year (+1y).
|
|
8
|
+
- Preserved raw provider-native values as `raw_forward_pe` and `raw_forward_eps` alongside `forward_pe_source` (`"FY1_CONSENSUS"` or `"RAW_FALLBACK"`).
|
|
9
|
+
- Implemented defensive price derivation and currency/share-class unit mismatch protection for ADRs (ratio guard between 0.25 and 3.0) to safely fall back to raw values.
|
|
10
|
+
- **GAAP vs. Non-GAAP Distortion Detection**:
|
|
11
|
+
- Integrated Non-GAAP core operating consensus from `info.get("epsCurrentYear")` (`eps_current_year`).
|
|
12
|
+
- Added algorithmic relative deviation calculation (`gaap_diff_pct = |eps_0y - eps_current_year| / min`) and distortion flagging (`has_gaap_distortion` when `gaap_diff_pct > 0.25`).
|
|
13
|
+
- Protects algorithmic screeners and downstream valuation pipelines from spin-off, M&A, and accounting windfall false-value traps (e.g. `GEV`).
|
|
14
|
+
|
|
3
15
|
## 0.2.1 — 2026-09-04
|
|
4
16
|
|
|
5
17
|
- Upgraded official `yfinance` provider lock from `1.5.1` to `1.7.0` (`ohmydata[yfinance]`):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ohmydata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Financial and alternative market data ingestion SDK.
|
|
5
5
|
Author: OMD contributors
|
|
6
6
|
Requires-Python: <3.13,>=3.11
|
|
@@ -26,6 +26,8 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# Oh My Data (OMD)
|
|
28
28
|
|
|
29
|
+
[English](README.md) | [中文说明](README_zh.md)
|
|
30
|
+
|
|
29
31
|
`ohmydata` is an offline-first market-data ingestion SDK and CLI. Provider endpoint
|
|
30
32
|
adapters accept already initialized official-compatible clients; credentials
|
|
31
33
|
are never loaded by this library.
|
|
@@ -40,8 +42,9 @@ uv run python -c "import ohmydata; print(ohmydata.__version__)"
|
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
The core has no runtime dependencies. Install `ohmydata[tushare]` for the
|
|
43
|
-
Pandas-backed Tushare adapter, `ohmydata[
|
|
44
|
-
|
|
45
|
+
Pandas-backed Tushare adapter, `ohmydata[yfinance]` for US/global market data and
|
|
46
|
+
fundamentals, `ohmydata[sec-cli]` for the SEC N-PORT batch CLI, or
|
|
47
|
+
`ohmydata[sec-financials]` for company 10-K/10-Q financial statements and Parquet
|
|
45
48
|
dataset writer. Provider tests use fake clients and never call a network.
|
|
46
49
|
|
|
47
50
|
## Core Architecture (offline & immutable)
|
|
@@ -503,6 +506,65 @@ preserve native line item labels and concepts (`concept`, `label`, `value_native
|
|
|
503
506
|
beside standardized XBRL categories (`standard_concept`) for cross-company
|
|
504
507
|
quantitative comparisons.
|
|
505
508
|
|
|
509
|
+
### yfinance (US & Global Market Data, Fundamentals, and Zero-Drift Audit)
|
|
510
|
+
|
|
511
|
+
Install `ohmydata[yfinance]` to access normalized market data, valuation ratios,
|
|
512
|
+
and financial statements with strict version pinning (`yfinance==1.7.0`):
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from ohmydata.providers.yfinance import (
|
|
516
|
+
YFinanceAdjustmentMode,
|
|
517
|
+
YFinanceBatchPolicy,
|
|
518
|
+
YFinanceClient,
|
|
519
|
+
YFinanceDailyBarsRequest,
|
|
520
|
+
YFinanceFundamentalsRequest,
|
|
521
|
+
YFinanceRepairPolicy,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
client = YFinanceClient()
|
|
525
|
+
|
|
526
|
+
# 1. Fetch normalized daily bars (OHLCV) with repair isolation
|
|
527
|
+
bars_req = YFinanceDailyBarsRequest(
|
|
528
|
+
symbols=("SPY", "QQQ", "^VIX"),
|
|
529
|
+
start_date="2024-01-01",
|
|
530
|
+
end_date_exclusive="2024-02-01",
|
|
531
|
+
adjustment_mode=YFinanceAdjustmentMode.RAW_WITH_ADJ_CLOSE,
|
|
532
|
+
batch_policy=YFinanceBatchPolicy.STRICT,
|
|
533
|
+
repair_policy=YFinanceRepairPolicy.PER_SYMBOL,
|
|
534
|
+
)
|
|
535
|
+
bars_result = client.fetch_daily_bars(bars_req)
|
|
536
|
+
df = bars_result.dataframe
|
|
537
|
+
|
|
538
|
+
# 2. Fetch fundamentals with institutional FY1 Forward P/E calibration
|
|
539
|
+
fund_req = YFinanceFundamentalsRequest(
|
|
540
|
+
symbols=("NVDA", "GEV"),
|
|
541
|
+
include_financials=True,
|
|
542
|
+
include_valuation=True,
|
|
543
|
+
include_estimates=True,
|
|
544
|
+
)
|
|
545
|
+
fund_result = client.fetch_fundamentals(fund_req)
|
|
546
|
+
|
|
547
|
+
nvda = fund_result.records["NVDA"]
|
|
548
|
+
# Forward P/E is calibrated to current year consensus (FY1 0y.avg) rather than out-year (+1y)
|
|
549
|
+
print("NVDA Calibrated FPE:", nvda.valuation.forward_pe, nvda.valuation.forward_pe_source)
|
|
550
|
+
print("NVDA Raw Yahoo FPE:", nvda.valuation.raw_forward_pe)
|
|
551
|
+
|
|
552
|
+
gev = fund_result.records["GEV"]
|
|
553
|
+
# GAAP vs Non-GAAP accounting distortion detection (flags one-off windfalls > 25%)
|
|
554
|
+
if gev.estimates.has_gaap_distortion:
|
|
555
|
+
print(f"GEV GAAP distortion flagged! Gap: {gev.estimates.gaap_diff_pct * 100:.1f}%")
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Zero-Drift Audit CLI (`omd audit-drift`)
|
|
559
|
+
|
|
560
|
+
Audit 10+ years of historical data against the 13-ETF `r10a0` benchmark universe before
|
|
561
|
+
any provider upgrade:
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
# Strict unadjusted market bar zero-drift gate (must be bit-exact 0.0 error)
|
|
565
|
+
uv run omd audit-drift --universe r10a0 --baseline-dir <old_version_dir> --target-dir <new_version_dir> --raw-only
|
|
566
|
+
```
|
|
567
|
+
|
|
506
568
|
## Dataframe Adapters (Polars & Pandas)
|
|
507
569
|
|
|
508
570
|
The optional `ohmydata[polars]` extra provides explicit, eager representation
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Oh My Data (OMD)
|
|
2
2
|
|
|
3
|
+
[English](README.md) | [中文说明](README_zh.md)
|
|
4
|
+
|
|
3
5
|
`ohmydata` is an offline-first market-data ingestion SDK and CLI. Provider endpoint
|
|
4
6
|
adapters accept already initialized official-compatible clients; credentials
|
|
5
7
|
are never loaded by this library.
|
|
@@ -14,8 +16,9 @@ uv run python -c "import ohmydata; print(ohmydata.__version__)"
|
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
The core has no runtime dependencies. Install `ohmydata[tushare]` for the
|
|
17
|
-
Pandas-backed Tushare adapter, `ohmydata[
|
|
18
|
-
|
|
19
|
+
Pandas-backed Tushare adapter, `ohmydata[yfinance]` for US/global market data and
|
|
20
|
+
fundamentals, `ohmydata[sec-cli]` for the SEC N-PORT batch CLI, or
|
|
21
|
+
`ohmydata[sec-financials]` for company 10-K/10-Q financial statements and Parquet
|
|
19
22
|
dataset writer. Provider tests use fake clients and never call a network.
|
|
20
23
|
|
|
21
24
|
## Core Architecture (offline & immutable)
|
|
@@ -477,6 +480,65 @@ preserve native line item labels and concepts (`concept`, `label`, `value_native
|
|
|
477
480
|
beside standardized XBRL categories (`standard_concept`) for cross-company
|
|
478
481
|
quantitative comparisons.
|
|
479
482
|
|
|
483
|
+
### yfinance (US & Global Market Data, Fundamentals, and Zero-Drift Audit)
|
|
484
|
+
|
|
485
|
+
Install `ohmydata[yfinance]` to access normalized market data, valuation ratios,
|
|
486
|
+
and financial statements with strict version pinning (`yfinance==1.7.0`):
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
from ohmydata.providers.yfinance import (
|
|
490
|
+
YFinanceAdjustmentMode,
|
|
491
|
+
YFinanceBatchPolicy,
|
|
492
|
+
YFinanceClient,
|
|
493
|
+
YFinanceDailyBarsRequest,
|
|
494
|
+
YFinanceFundamentalsRequest,
|
|
495
|
+
YFinanceRepairPolicy,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
client = YFinanceClient()
|
|
499
|
+
|
|
500
|
+
# 1. Fetch normalized daily bars (OHLCV) with repair isolation
|
|
501
|
+
bars_req = YFinanceDailyBarsRequest(
|
|
502
|
+
symbols=("SPY", "QQQ", "^VIX"),
|
|
503
|
+
start_date="2024-01-01",
|
|
504
|
+
end_date_exclusive="2024-02-01",
|
|
505
|
+
adjustment_mode=YFinanceAdjustmentMode.RAW_WITH_ADJ_CLOSE,
|
|
506
|
+
batch_policy=YFinanceBatchPolicy.STRICT,
|
|
507
|
+
repair_policy=YFinanceRepairPolicy.PER_SYMBOL,
|
|
508
|
+
)
|
|
509
|
+
bars_result = client.fetch_daily_bars(bars_req)
|
|
510
|
+
df = bars_result.dataframe
|
|
511
|
+
|
|
512
|
+
# 2. Fetch fundamentals with institutional FY1 Forward P/E calibration
|
|
513
|
+
fund_req = YFinanceFundamentalsRequest(
|
|
514
|
+
symbols=("NVDA", "GEV"),
|
|
515
|
+
include_financials=True,
|
|
516
|
+
include_valuation=True,
|
|
517
|
+
include_estimates=True,
|
|
518
|
+
)
|
|
519
|
+
fund_result = client.fetch_fundamentals(fund_req)
|
|
520
|
+
|
|
521
|
+
nvda = fund_result.records["NVDA"]
|
|
522
|
+
# Forward P/E is calibrated to current year consensus (FY1 0y.avg) rather than out-year (+1y)
|
|
523
|
+
print("NVDA Calibrated FPE:", nvda.valuation.forward_pe, nvda.valuation.forward_pe_source)
|
|
524
|
+
print("NVDA Raw Yahoo FPE:", nvda.valuation.raw_forward_pe)
|
|
525
|
+
|
|
526
|
+
gev = fund_result.records["GEV"]
|
|
527
|
+
# GAAP vs Non-GAAP accounting distortion detection (flags one-off windfalls > 25%)
|
|
528
|
+
if gev.estimates.has_gaap_distortion:
|
|
529
|
+
print(f"GEV GAAP distortion flagged! Gap: {gev.estimates.gaap_diff_pct * 100:.1f}%")
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Zero-Drift Audit CLI (`omd audit-drift`)
|
|
533
|
+
|
|
534
|
+
Audit 10+ years of historical data against the 13-ETF `r10a0` benchmark universe before
|
|
535
|
+
any provider upgrade:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
# Strict unadjusted market bar zero-drift gate (must be bit-exact 0.0 error)
|
|
539
|
+
uv run omd audit-drift --universe r10a0 --baseline-dir <old_version_dir> --target-dir <new_version_dir> --raw-only
|
|
540
|
+
```
|
|
541
|
+
|
|
480
542
|
## Dataframe Adapters (Polars & Pandas)
|
|
481
543
|
|
|
482
544
|
The optional `ohmydata[polars]` extra provides explicit, eager representation
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Internal helper functions for parsing financial statements, valuation, and analyst estimates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _safe_float(val: Any) -> float | None:
|
|
12
|
+
"""Convert arbitrary numeric value to float or None."""
|
|
13
|
+
try:
|
|
14
|
+
if val is None or pd.isna(val):
|
|
15
|
+
return None
|
|
16
|
+
f = float(val)
|
|
17
|
+
return f if pd.notna(f) else None
|
|
18
|
+
except (ValueError, TypeError):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_quarterly_pair(series: pd.Series | None) -> tuple[float | None, float | None]:
|
|
23
|
+
"""Return (latest_quarter, same_quarter_last_year) from a quarterly series."""
|
|
24
|
+
if series is None or series.empty:
|
|
25
|
+
return (None, None)
|
|
26
|
+
cleaned = pd.to_numeric(series, errors="coerce").dropna()
|
|
27
|
+
if len(cleaned) < 5:
|
|
28
|
+
# If at least 1 quarter exists, return latest, but prev_year is None
|
|
29
|
+
latest = _safe_float(cleaned.iloc[0]) if len(cleaned) >= 1 else None
|
|
30
|
+
return (latest, None)
|
|
31
|
+
return (_safe_float(cleaned.iloc[0]), _safe_float(cleaned.iloc[4]))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_metric_pair(
|
|
35
|
+
stmt: pd.DataFrame | None, candidate_keys: list[str]
|
|
36
|
+
) -> tuple[float | None, float | None]:
|
|
37
|
+
"""Search for metric rows across aliases and extract (latest, prev_year)."""
|
|
38
|
+
if stmt is None or stmt.empty:
|
|
39
|
+
return (None, None)
|
|
40
|
+
# Search index names ignoring case and whitespace
|
|
41
|
+
norm_index = {str(idx).strip().lower(): idx for idx in stmt.index}
|
|
42
|
+
for candidate in candidate_keys:
|
|
43
|
+
cand_key = candidate.strip().lower()
|
|
44
|
+
if cand_key in norm_index:
|
|
45
|
+
row_key = norm_index[cand_key]
|
|
46
|
+
return extract_quarterly_pair(stmt.loc[row_key])
|
|
47
|
+
return (None, None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_report_date(
|
|
51
|
+
info: dict[str, Any] | None, income_stmt: pd.DataFrame | None
|
|
52
|
+
) -> datetime.date | None:
|
|
53
|
+
"""Extract report date from mostRecentQuarter or statement columns, rejecting epoch 0."""
|
|
54
|
+
info = info or {}
|
|
55
|
+
candidates: list[datetime.date] = []
|
|
56
|
+
|
|
57
|
+
mrq = info.get("mostRecentQuarter")
|
|
58
|
+
if mrq is not None:
|
|
59
|
+
try:
|
|
60
|
+
if isinstance(mrq, (int, float)) and mrq > 0:
|
|
61
|
+
dt = datetime.datetime.fromtimestamp(mrq, tz=datetime.UTC).date()
|
|
62
|
+
candidates.append(dt)
|
|
63
|
+
elif isinstance(mrq, str):
|
|
64
|
+
dt = pd.to_datetime(mrq).date()
|
|
65
|
+
candidates.append(dt)
|
|
66
|
+
except (ValueError, TypeError, OSError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
if income_stmt is not None and not income_stmt.empty:
|
|
70
|
+
for col in income_stmt.columns:
|
|
71
|
+
try:
|
|
72
|
+
dt = pd.to_datetime(col).date()
|
|
73
|
+
candidates.append(dt)
|
|
74
|
+
break
|
|
75
|
+
except (ValueError, TypeError, OSError):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
for dt in candidates:
|
|
79
|
+
# Must be after 1990-01-01 and not epoch 1970-01-01
|
|
80
|
+
if dt.year >= 1990:
|
|
81
|
+
return dt
|
|
82
|
+
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_estimates_horizons(
|
|
87
|
+
df_est: pd.DataFrame | None,
|
|
88
|
+
) -> tuple[float | None, float | None, float | None, float | None]:
|
|
89
|
+
"""Extract (current_q, next_q, current_y, next_y) from estimate DataFrame."""
|
|
90
|
+
if df_est is None or df_est.empty:
|
|
91
|
+
return (None, None, None, None)
|
|
92
|
+
|
|
93
|
+
# Clean index
|
|
94
|
+
df_clean = df_est.copy()
|
|
95
|
+
df_clean.index = [str(idx).strip().lower() for idx in df_clean.index]
|
|
96
|
+
|
|
97
|
+
# Find avg / mean column
|
|
98
|
+
target_col = None
|
|
99
|
+
for col in df_clean.columns:
|
|
100
|
+
c_low = str(col).lower()
|
|
101
|
+
if "avg" in c_low or "mean" in c_low:
|
|
102
|
+
target_col = col
|
|
103
|
+
break
|
|
104
|
+
if target_col is None and len(df_clean.columns) > 0:
|
|
105
|
+
target_col = df_clean.columns[0]
|
|
106
|
+
|
|
107
|
+
if target_col is None:
|
|
108
|
+
return (None, None, None, None)
|
|
109
|
+
|
|
110
|
+
series = pd.to_numeric(df_clean[target_col], errors="coerce")
|
|
111
|
+
|
|
112
|
+
def _get_val(keys: list[str]) -> float | None:
|
|
113
|
+
for k in keys:
|
|
114
|
+
if k in series.index:
|
|
115
|
+
val = series.loc[k]
|
|
116
|
+
return _safe_float(val)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
cq = _get_val(["0q", "current quarter", "currentq"])
|
|
120
|
+
nq = _get_val(["+1q", "next quarter", "nextq", "1q"])
|
|
121
|
+
cy = _get_val(["0y", "current year", "currenty"])
|
|
122
|
+
ny = _get_val(["+1y", "next year", "nexty", "1y"])
|
|
123
|
+
return (cq, nq, cy, ny)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def derive_implied_price(
|
|
127
|
+
raw_forward_pe: float | None,
|
|
128
|
+
raw_forward_eps: float | None,
|
|
129
|
+
trailing_pe: float | None,
|
|
130
|
+
deps_latest: float | None,
|
|
131
|
+
market_cap: float | None,
|
|
132
|
+
shares_outstanding: float | None,
|
|
133
|
+
) -> float | None:
|
|
134
|
+
"""Derive implied market price defensively across priority cascade."""
|
|
135
|
+
if (
|
|
136
|
+
raw_forward_pe is not None
|
|
137
|
+
and raw_forward_eps is not None
|
|
138
|
+
and raw_forward_pe > 0
|
|
139
|
+
and raw_forward_eps > 0
|
|
140
|
+
):
|
|
141
|
+
return raw_forward_pe * raw_forward_eps
|
|
142
|
+
if trailing_pe is not None and deps_latest is not None and trailing_pe > 0 and deps_latest > 0:
|
|
143
|
+
return trailing_pe * deps_latest
|
|
144
|
+
if market_cap is not None and shares_outstanding is not None and shares_outstanding > 0:
|
|
145
|
+
return market_cap / shares_outstanding
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def calibrate_forward_pe(
|
|
150
|
+
implied_price: float | None,
|
|
151
|
+
raw_forward_pe: float | None,
|
|
152
|
+
raw_forward_eps: float | None,
|
|
153
|
+
eps_0y: float | None,
|
|
154
|
+
) -> tuple[float | None, float | None, str | None]:
|
|
155
|
+
"""Calibrate institutional FY1 Forward P/E against sell-side consensus with ADR protection.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
tuple of (calibrated_forward_pe, calibrated_forward_eps, forward_pe_source)
|
|
159
|
+
"""
|
|
160
|
+
calibrated_fpe = raw_forward_pe
|
|
161
|
+
calibrated_feps = raw_forward_eps
|
|
162
|
+
fpe_source = "RAW_FALLBACK" if raw_forward_pe is not None else None
|
|
163
|
+
|
|
164
|
+
if implied_price is not None and eps_0y is not None and eps_0y > 0:
|
|
165
|
+
is_mismatched = False
|
|
166
|
+
if raw_forward_eps is not None and raw_forward_eps > 0:
|
|
167
|
+
ratio = eps_0y / raw_forward_eps
|
|
168
|
+
if ratio > 3.0 or ratio < 0.25:
|
|
169
|
+
is_mismatched = True
|
|
170
|
+
|
|
171
|
+
if not is_mismatched:
|
|
172
|
+
calibrated_fpe = round(implied_price / eps_0y, 4)
|
|
173
|
+
calibrated_feps = round(eps_0y, 4)
|
|
174
|
+
fpe_source = "FY1_CONSENSUS"
|
|
175
|
+
|
|
176
|
+
return (calibrated_fpe, calibrated_feps, fpe_source)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def detect_gaap_distortion(
|
|
180
|
+
eps_0y: float | None,
|
|
181
|
+
eps_current_year: float | None,
|
|
182
|
+
) -> tuple[float | None, bool]:
|
|
183
|
+
"""Detect relative GAAP vs Non-GAAP consensus divergence (e.g. spin-offs, M&A windfalls).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
tuple of (gaap_diff_pct, has_gaap_distortion)
|
|
187
|
+
"""
|
|
188
|
+
gaap_diff_pct = None
|
|
189
|
+
has_gaap_distortion = False
|
|
190
|
+
|
|
191
|
+
if eps_0y is not None and eps_current_year is not None and eps_0y > 0 and eps_current_year > 0:
|
|
192
|
+
denom = min(eps_0y, eps_current_year)
|
|
193
|
+
if denom > 0:
|
|
194
|
+
gap = abs(eps_0y - eps_current_year) / denom
|
|
195
|
+
gaap_diff_pct = round(gap, 4)
|
|
196
|
+
if gap > 0.25:
|
|
197
|
+
has_gaap_distortion = True
|
|
198
|
+
|
|
199
|
+
return (gaap_diff_pct, has_gaap_distortion)
|
|
@@ -12,6 +12,16 @@ import pandas as pd
|
|
|
12
12
|
from ohmydata.core.policy import RetryPolicy
|
|
13
13
|
from ohmydata.core.provenance import FetchProvenance
|
|
14
14
|
|
|
15
|
+
from ._fundamentals_parsers import (
|
|
16
|
+
_safe_float,
|
|
17
|
+
calibrate_forward_pe,
|
|
18
|
+
derive_implied_price,
|
|
19
|
+
detect_gaap_distortion,
|
|
20
|
+
extract_estimates_horizons,
|
|
21
|
+
extract_metric_pair,
|
|
22
|
+
extract_quarterly_pair,
|
|
23
|
+
extract_report_date,
|
|
24
|
+
)
|
|
15
25
|
from .endpoints import validate_yfinance_symbol
|
|
16
26
|
|
|
17
27
|
NON_EQUITY_QUOTE_TYPES = frozenset(
|
|
@@ -19,90 +29,18 @@ NON_EQUITY_QUOTE_TYPES = frozenset(
|
|
|
19
29
|
)
|
|
20
30
|
|
|
21
31
|
|
|
22
|
-
def _safe_float(val: Any) -> float | None:
|
|
23
|
-
"""Convert arbitrary numeric value to float or None."""
|
|
24
|
-
try:
|
|
25
|
-
if val is None or pd.isna(val):
|
|
26
|
-
return None
|
|
27
|
-
f = float(val)
|
|
28
|
-
return f if pd.notna(f) else None
|
|
29
|
-
except (ValueError, TypeError):
|
|
30
|
-
return None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def extract_quarterly_pair(series: pd.Series | None) -> tuple[float | None, float | None]:
|
|
34
|
-
"""Return (latest_quarter, same_quarter_last_year) from a quarterly series."""
|
|
35
|
-
if series is None or series.empty:
|
|
36
|
-
return (None, None)
|
|
37
|
-
cleaned = pd.to_numeric(series, errors="coerce").dropna()
|
|
38
|
-
if len(cleaned) < 5:
|
|
39
|
-
# If at least 1 quarter exists, return latest, but prev_year is None
|
|
40
|
-
latest = _safe_float(cleaned.iloc[0]) if len(cleaned) >= 1 else None
|
|
41
|
-
return (latest, None)
|
|
42
|
-
return (_safe_float(cleaned.iloc[0]), _safe_float(cleaned.iloc[4]))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def extract_metric_pair(
|
|
46
|
-
stmt: pd.DataFrame | None, candidate_keys: list[str]
|
|
47
|
-
) -> tuple[float | None, float | None]:
|
|
48
|
-
"""Search for metric rows across aliases and extract (latest, prev_year)."""
|
|
49
|
-
if stmt is None or stmt.empty:
|
|
50
|
-
return (None, None)
|
|
51
|
-
# Search index names ignoring case and whitespace
|
|
52
|
-
norm_index = {str(idx).strip().lower(): idx for idx in stmt.index}
|
|
53
|
-
for candidate in candidate_keys:
|
|
54
|
-
cand_key = candidate.strip().lower()
|
|
55
|
-
if cand_key in norm_index:
|
|
56
|
-
row_key = norm_index[cand_key]
|
|
57
|
-
return extract_quarterly_pair(stmt.loc[row_key])
|
|
58
|
-
return (None, None)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def extract_report_date(
|
|
62
|
-
info: dict[str, Any] | None, income_stmt: pd.DataFrame | None
|
|
63
|
-
) -> datetime.date | None:
|
|
64
|
-
"""Extract report date from mostRecentQuarter or statement columns, rejecting epoch 0."""
|
|
65
|
-
info = info or {}
|
|
66
|
-
candidates: list[datetime.date] = []
|
|
67
|
-
|
|
68
|
-
mrq = info.get("mostRecentQuarter")
|
|
69
|
-
if mrq is not None:
|
|
70
|
-
try:
|
|
71
|
-
if isinstance(mrq, (int, float)) and mrq > 0:
|
|
72
|
-
dt = datetime.datetime.fromtimestamp(mrq, tz=datetime.UTC).date()
|
|
73
|
-
candidates.append(dt)
|
|
74
|
-
elif isinstance(mrq, str):
|
|
75
|
-
dt = pd.to_datetime(mrq).date()
|
|
76
|
-
candidates.append(dt)
|
|
77
|
-
except (ValueError, TypeError, OSError):
|
|
78
|
-
pass
|
|
79
|
-
|
|
80
|
-
if income_stmt is not None and not income_stmt.empty:
|
|
81
|
-
for col in income_stmt.columns:
|
|
82
|
-
try:
|
|
83
|
-
dt = pd.to_datetime(col).date()
|
|
84
|
-
candidates.append(dt)
|
|
85
|
-
break
|
|
86
|
-
except (ValueError, TypeError, OSError):
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
for dt in candidates:
|
|
90
|
-
# Must be after 1990-01-01 and not epoch 1970-01-01
|
|
91
|
-
if dt.year >= 1990:
|
|
92
|
-
return dt
|
|
93
|
-
|
|
94
|
-
return None
|
|
95
|
-
|
|
96
|
-
|
|
97
32
|
@dataclass(frozen=True)
|
|
98
33
|
class YFinanceValuationSnapshot:
|
|
99
34
|
"""Snapshot of valuation ratios and market size."""
|
|
100
35
|
|
|
101
36
|
trailing_pe: float | None = None
|
|
102
|
-
forward_pe: float | None = None
|
|
37
|
+
forward_pe: float | None = None # Calibrated FY1 institutional Forward P/E
|
|
38
|
+
raw_forward_pe: float | None = None # Provider-native raw value from info.get("forwardPE")
|
|
39
|
+
forward_pe_source: str | None = None # "FY1_CONSENSUS" | "RAW_FALLBACK" | None
|
|
103
40
|
peg_ratio: float | None = None
|
|
104
41
|
price_to_sales: float | None = None
|
|
105
|
-
forward_eps: float | None = None
|
|
42
|
+
forward_eps: float | None = None # Calibrated FY1 forward EPS (or raw if uncalibrated)
|
|
43
|
+
raw_forward_eps: float | None = None # Provider-native raw value from info.get("forwardEps")
|
|
106
44
|
market_cap: float | None = None
|
|
107
45
|
enterprise_value: float | None = None
|
|
108
46
|
shares_outstanding: float | None = None
|
|
@@ -147,7 +85,7 @@ class YFinanceQuarterlyFinancials:
|
|
|
147
85
|
|
|
148
86
|
@dataclass(frozen=True)
|
|
149
87
|
class YFinanceAnalystEstimates:
|
|
150
|
-
"""Analyst consensus estimates across
|
|
88
|
+
"""Analyst consensus estimates across horizons and Non-GAAP cross-validation."""
|
|
151
89
|
|
|
152
90
|
revenue_est_current_q: float | None = None
|
|
153
91
|
revenue_est_next_q: float | None = None
|
|
@@ -155,8 +93,13 @@ class YFinanceAnalystEstimates:
|
|
|
155
93
|
revenue_est_next_y: float | None = None
|
|
156
94
|
eps_est_current_q: float | None = None
|
|
157
95
|
eps_est_next_q: float | None = None
|
|
158
|
-
eps_est_current_y: float | None = None
|
|
159
|
-
eps_est_next_y: float | None = None
|
|
96
|
+
eps_est_current_y: float | None = None # Sell-side consensus 0y.avg (often GAAP in Yahoo)
|
|
97
|
+
eps_est_next_y: float | None = None # Sell-side consensus +1y.avg
|
|
98
|
+
eps_current_year: float | None = (
|
|
99
|
+
None # Core operating consensus from info.get("epsCurrentYear") (Non-GAAP)
|
|
100
|
+
)
|
|
101
|
+
gaap_diff_pct: float | None = None # Relative deviation: |eps_0y - eps_current_y| / min
|
|
102
|
+
has_gaap_distortion: bool = False # True when gaap_diff_pct > 0.25
|
|
160
103
|
|
|
161
104
|
|
|
162
105
|
@dataclass(frozen=True)
|
|
@@ -236,46 +179,6 @@ class YFinanceFundamentalsResult:
|
|
|
236
179
|
return pd.DataFrame(self.to_records())
|
|
237
180
|
|
|
238
181
|
|
|
239
|
-
def extract_estimates_horizons(
|
|
240
|
-
df_est: pd.DataFrame | None,
|
|
241
|
-
) -> tuple[float | None, float | None, float | None, float | None]:
|
|
242
|
-
"""Extract (current_q, next_q, current_y, next_y) from estimate DataFrame."""
|
|
243
|
-
if df_est is None or df_est.empty:
|
|
244
|
-
return (None, None, None, None)
|
|
245
|
-
|
|
246
|
-
# Clean index
|
|
247
|
-
df_clean = df_est.copy()
|
|
248
|
-
df_clean.index = [str(idx).strip().lower() for idx in df_clean.index]
|
|
249
|
-
|
|
250
|
-
# Find avg / mean column
|
|
251
|
-
target_col = None
|
|
252
|
-
for col in df_clean.columns:
|
|
253
|
-
c_low = str(col).lower()
|
|
254
|
-
if "avg" in c_low or "mean" in c_low:
|
|
255
|
-
target_col = col
|
|
256
|
-
break
|
|
257
|
-
if target_col is None and len(df_clean.columns) > 0:
|
|
258
|
-
target_col = df_clean.columns[0]
|
|
259
|
-
|
|
260
|
-
if target_col is None:
|
|
261
|
-
return (None, None, None, None)
|
|
262
|
-
|
|
263
|
-
series = pd.to_numeric(df_clean[target_col], errors="coerce")
|
|
264
|
-
|
|
265
|
-
def _get_val(keys: list[str]) -> float | None:
|
|
266
|
-
for k in keys:
|
|
267
|
-
if k in series.index:
|
|
268
|
-
val = series.loc[k]
|
|
269
|
-
return _safe_float(val)
|
|
270
|
-
return None
|
|
271
|
-
|
|
272
|
-
cq = _get_val(["0q", "current quarter", "currentq"])
|
|
273
|
-
nq = _get_val(["+1q", "next quarter", "nextq", "1q"])
|
|
274
|
-
cy = _get_val(["0y", "current year", "currenty"])
|
|
275
|
-
ny = _get_val(["+1y", "next year", "nexty", "1y"])
|
|
276
|
-
return (cq, nq, cy, ny)
|
|
277
|
-
|
|
278
|
-
|
|
279
182
|
def parse_symbol_fundamentals(
|
|
280
183
|
symbol: str,
|
|
281
184
|
info: dict[str, Any] | None,
|
|
@@ -312,17 +215,10 @@ def parse_symbol_fundamentals(
|
|
|
312
215
|
str(info.get("currency") or info.get("financialCurrency") or "").strip().upper() or None
|
|
313
216
|
)
|
|
314
217
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
price_to_sales=_safe_float(info.get("priceToSalesTrailing12Months")),
|
|
320
|
-
forward_eps=_safe_float(info.get("forwardEps")),
|
|
321
|
-
market_cap=market_cap,
|
|
322
|
-
enterprise_value=_safe_float(info.get("enterpriseValue")),
|
|
323
|
-
shares_outstanding=shares_outstanding,
|
|
324
|
-
currency=currency,
|
|
325
|
-
)
|
|
218
|
+
trailing_pe = _safe_float(info.get("trailingPE"))
|
|
219
|
+
raw_forward_pe = _safe_float(info.get("forwardPE"))
|
|
220
|
+
raw_forward_eps = _safe_float(info.get("forwardEps"))
|
|
221
|
+
eps_current_year = _safe_float(info.get("epsCurrentYear"))
|
|
326
222
|
|
|
327
223
|
# Extract statements YoY pairs
|
|
328
224
|
rev_latest, rev_prev = extract_metric_pair(
|
|
@@ -349,19 +245,16 @@ def parse_symbol_fundamentals(
|
|
|
349
245
|
],
|
|
350
246
|
)
|
|
351
247
|
fcf_latest, fcf_prev = extract_metric_pair(cashflow_stmt, ["Free Cash Flow"])
|
|
352
|
-
capex_latest, capex_prev = extract_metric_pair(
|
|
353
|
-
cashflow_stmt, ["Capital Expenditure", "Capital Expenditures"]
|
|
354
|
-
)
|
|
248
|
+
capex_latest, capex_prev = extract_metric_pair(cashflow_stmt, ["Capital Expenditure"])
|
|
355
249
|
|
|
356
250
|
debt_latest, debt_prev = extract_metric_pair(balance_stmt, ["Total Debt"])
|
|
357
251
|
cash_latest, cash_prev = extract_metric_pair(
|
|
358
252
|
balance_stmt,
|
|
359
|
-
["Cash And Cash Equivalents", "Cash Cash Equivalents And Short Term Investments"
|
|
253
|
+
["Cash And Cash Equivalents", "Cash Cash Equivalents And Short Term Investments"],
|
|
360
254
|
)
|
|
361
255
|
net_debt_latest, net_debt_prev = extract_metric_pair(balance_stmt, ["Net Debt"])
|
|
362
256
|
equity_latest, equity_prev = extract_metric_pair(
|
|
363
|
-
balance_stmt,
|
|
364
|
-
["Stockholders Equity", "Total Stockholder Equity", "Common Stock Equity"],
|
|
257
|
+
balance_stmt, ["Stockholders Equity", "Total Equity Gross Minority Interest"]
|
|
365
258
|
)
|
|
366
259
|
|
|
367
260
|
financials = YFinanceQuarterlyFinancials(
|
|
@@ -400,6 +293,46 @@ def parse_symbol_fundamentals(
|
|
|
400
293
|
# Extract estimates
|
|
401
294
|
rcq, rnq, rcy, rny = extract_estimates_horizons(rev_estimate_df)
|
|
402
295
|
ecq, enq, ecy, eny = extract_estimates_horizons(eps_estimate_df)
|
|
296
|
+
eps_0y = ecy
|
|
297
|
+
|
|
298
|
+
# Derive implied price defensively
|
|
299
|
+
implied_price = derive_implied_price(
|
|
300
|
+
raw_forward_pe=raw_forward_pe,
|
|
301
|
+
raw_forward_eps=raw_forward_eps,
|
|
302
|
+
trailing_pe=trailing_pe,
|
|
303
|
+
deps_latest=deps_latest,
|
|
304
|
+
market_cap=market_cap,
|
|
305
|
+
shares_outstanding=shares_outstanding,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Calibrate FY1 Forward P/E
|
|
309
|
+
calibrated_fpe, calibrated_feps, fpe_source = calibrate_forward_pe(
|
|
310
|
+
implied_price=implied_price,
|
|
311
|
+
raw_forward_pe=raw_forward_pe,
|
|
312
|
+
raw_forward_eps=raw_forward_eps,
|
|
313
|
+
eps_0y=eps_0y,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# GAAP vs. Non-GAAP Distortion Detection
|
|
317
|
+
gaap_diff_pct, has_gaap_distortion = detect_gaap_distortion(
|
|
318
|
+
eps_0y=eps_0y,
|
|
319
|
+
eps_current_year=eps_current_year,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
valuation = YFinanceValuationSnapshot(
|
|
323
|
+
trailing_pe=trailing_pe,
|
|
324
|
+
forward_pe=calibrated_fpe,
|
|
325
|
+
raw_forward_pe=raw_forward_pe,
|
|
326
|
+
forward_pe_source=fpe_source,
|
|
327
|
+
peg_ratio=_safe_float(info.get("pegRatio")),
|
|
328
|
+
price_to_sales=_safe_float(info.get("priceToSalesTrailing12Months")),
|
|
329
|
+
forward_eps=calibrated_feps,
|
|
330
|
+
raw_forward_eps=raw_forward_eps,
|
|
331
|
+
market_cap=market_cap,
|
|
332
|
+
enterprise_value=_safe_float(info.get("enterpriseValue")),
|
|
333
|
+
shares_outstanding=shares_outstanding,
|
|
334
|
+
currency=currency,
|
|
335
|
+
)
|
|
403
336
|
|
|
404
337
|
estimates = YFinanceAnalystEstimates(
|
|
405
338
|
revenue_est_current_q=rcq,
|
|
@@ -410,6 +343,9 @@ def parse_symbol_fundamentals(
|
|
|
410
343
|
eps_est_next_q=enq,
|
|
411
344
|
eps_est_current_y=ecy,
|
|
412
345
|
eps_est_next_y=eny,
|
|
346
|
+
eps_current_year=eps_current_year,
|
|
347
|
+
gaap_diff_pct=gaap_diff_pct,
|
|
348
|
+
has_gaap_distortion=has_gaap_distortion,
|
|
413
349
|
)
|
|
414
350
|
|
|
415
351
|
report_dt = extract_report_date(info, income_stmt)
|
|
@@ -420,9 +356,27 @@ def parse_symbol_fundamentals(
|
|
|
420
356
|
quote_type=quote_type,
|
|
421
357
|
is_excluded=is_excluded,
|
|
422
358
|
exclusion_reason=exclusion_reason,
|
|
423
|
-
revenue_growth_hint=_safe_float(info.get("revenueGrowth")),
|
|
424
|
-
eps_growth_hint=_safe_float(info.get("earningsGrowth")),
|
|
425
359
|
valuation=valuation,
|
|
426
360
|
financials=financials,
|
|
427
361
|
estimates=estimates,
|
|
428
362
|
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
__all__ = [
|
|
366
|
+
"NON_EQUITY_QUOTE_TYPES",
|
|
367
|
+
"YFinanceAnalystEstimates",
|
|
368
|
+
"YFinanceFundamentalsRequest",
|
|
369
|
+
"YFinanceFundamentalsResult",
|
|
370
|
+
"YFinanceQuarterlyFinancials",
|
|
371
|
+
"YFinanceSymbolFundamentals",
|
|
372
|
+
"YFinanceValuationSnapshot",
|
|
373
|
+
"_safe_float",
|
|
374
|
+
"calibrate_forward_pe",
|
|
375
|
+
"derive_implied_price",
|
|
376
|
+
"detect_gaap_distortion",
|
|
377
|
+
"extract_estimates_horizons",
|
|
378
|
+
"extract_metric_pair",
|
|
379
|
+
"extract_quarterly_pair",
|
|
380
|
+
"extract_report_date",
|
|
381
|
+
"parse_symbol_fundamentals",
|
|
382
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/observations/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/etf_adjusted_bars.py
RENAMED
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/etf_index_mapping.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/index_weight_vintage.py
RENAMED
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/lookthrough_bundle.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.1 → ohmydata-0.2.2}/src/ohmydata/providers/tushare/recipes/weighted_dividend_yield.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|