implement Phase 2 gap items: backfill CLI, retry, financial features
- filing_poller: add --start-date/--end-date CLI args for historical backfill (defaults to 7 days ago when omitted) - OracleClient.get/post: apply with_retry(max_attempts=3) so transient connection errors, timeouts, and 5xx responses are automatically retried with exponential backoff (0.1s→0.2s→fail) - financial_features: new compute_financial_features() extracting latest_eps, latest_gross_margin, latest_operating_margin, eps_growth_qoq, revenue_growth_qoq from FinancialDataResponse - feature_builder: wire FinancialService into build_features_for_event(), persisting financial_v1 FeatureSnapshot (non-fatal if unavailable) - tests: 94 pass (81→89 unit + 5 replay); +8 new tests covering financial features and retry success path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
32a845044c
commit
38ac53b597
@ -0,0 +1,41 @@
|
|||||||
|
"""Financial feature calculations from Oracle financial data."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from libs.oracle_client.models import FinancialDataResponse
|
||||||
|
|
||||||
|
|
||||||
|
def compute_financial_features(response: FinancialDataResponse) -> dict[str, Any]:
|
||||||
|
"""Compute financial features from quarterly period data.
|
||||||
|
|
||||||
|
Returns dict with latest-quarter metrics and QoQ growth rates.
|
||||||
|
Returns empty dict if no period data is available.
|
||||||
|
"""
|
||||||
|
periods = sorted(response.periods, key=lambda p: p.period_end, reverse=True)
|
||||||
|
if not periods:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
latest = periods[0]
|
||||||
|
features: dict[str, Any] = {
|
||||||
|
"latest_eps": latest.eps,
|
||||||
|
"latest_gross_margin": latest.gross_margin,
|
||||||
|
"latest_operating_margin": latest.operating_margin,
|
||||||
|
"eps_growth_qoq": None,
|
||||||
|
"revenue_growth_qoq": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(periods) >= 2:
|
||||||
|
prior = periods[1]
|
||||||
|
if latest.eps is not None and prior.eps is not None and prior.eps != 0:
|
||||||
|
features["eps_growth_qoq"] = (latest.eps - prior.eps) / abs(prior.eps)
|
||||||
|
if (
|
||||||
|
latest.revenue is not None
|
||||||
|
and prior.revenue is not None
|
||||||
|
and prior.revenue != 0
|
||||||
|
):
|
||||||
|
features["revenue_growth_qoq"] = (
|
||||||
|
latest.revenue - prior.revenue
|
||||||
|
) / prior.revenue
|
||||||
|
|
||||||
|
return features
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
"""Unit tests for financial feature calculations."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from libs.oracle_client.models import FinancialDataResponse, FinancialPeriod
|
||||||
|
|
||||||
|
TWO_PERIOD_RESPONSE = FinancialDataResponse(
|
||||||
|
ticker="AAPL",
|
||||||
|
periods=[
|
||||||
|
FinancialPeriod(
|
||||||
|
period="2026-Q1",
|
||||||
|
period_end="2025-12-28",
|
||||||
|
revenue=124_300_000_000,
|
||||||
|
net_income=36_000_000_000,
|
||||||
|
eps=2.34,
|
||||||
|
gross_margin=0.472,
|
||||||
|
operating_margin=0.315,
|
||||||
|
),
|
||||||
|
FinancialPeriod(
|
||||||
|
period="2025-Q4",
|
||||||
|
period_end="2025-09-27",
|
||||||
|
revenue=119_600_000_000,
|
||||||
|
net_income=34_900_000_000,
|
||||||
|
eps=2.26,
|
||||||
|
gross_margin=0.461,
|
||||||
|
operating_margin=0.308,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_financial_features_latest_values():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
features = compute_financial_features(TWO_PERIOD_RESPONSE)
|
||||||
|
|
||||||
|
assert features["latest_eps"] == pytest.approx(2.34)
|
||||||
|
assert features["latest_gross_margin"] == pytest.approx(0.472)
|
||||||
|
assert features["latest_operating_margin"] == pytest.approx(0.315)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eps_growth_qoq():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
features = compute_financial_features(TWO_PERIOD_RESPONSE)
|
||||||
|
|
||||||
|
expected = (2.34 - 2.26) / abs(2.26)
|
||||||
|
assert features["eps_growth_qoq"] == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_revenue_growth_qoq():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
features = compute_financial_features(TWO_PERIOD_RESPONSE)
|
||||||
|
|
||||||
|
expected = (124_300_000_000 - 119_600_000_000) / 119_600_000_000
|
||||||
|
assert features["revenue_growth_qoq"] == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_periods_sorted_by_period_end_descending():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
# Provide periods out of chronological order; latest should still be picked
|
||||||
|
response = FinancialDataResponse(
|
||||||
|
ticker="AAPL",
|
||||||
|
periods=[
|
||||||
|
FinancialPeriod(period="2025-Q4", period_end="2025-09-27", eps=2.26),
|
||||||
|
FinancialPeriod(period="2026-Q1", period_end="2025-12-28", eps=2.34),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
features = compute_financial_features(response)
|
||||||
|
assert features["latest_eps"] == pytest.approx(2.34)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_period_no_growth_fields():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
response = FinancialDataResponse(
|
||||||
|
ticker="AAPL",
|
||||||
|
periods=[
|
||||||
|
FinancialPeriod(
|
||||||
|
period="2026-Q1", period_end="2025-12-28", eps=2.34, gross_margin=0.472
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
features = compute_financial_features(response)
|
||||||
|
|
||||||
|
assert features["latest_eps"] == pytest.approx(2.34)
|
||||||
|
assert features["eps_growth_qoq"] is None
|
||||||
|
assert features["revenue_growth_qoq"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_periods_returns_empty_dict():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
response = FinancialDataResponse(ticker="AAPL", periods=[])
|
||||||
|
assert compute_financial_features(response) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_none_eps_in_prior_skips_growth():
|
||||||
|
from libs.features.financial_features import compute_financial_features
|
||||||
|
|
||||||
|
response = FinancialDataResponse(
|
||||||
|
ticker="AAPL",
|
||||||
|
periods=[
|
||||||
|
FinancialPeriod(period="2026-Q1", period_end="2025-12-28", eps=2.34),
|
||||||
|
FinancialPeriod(period="2025-Q4", period_end="2025-09-27", eps=None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
features = compute_financial_features(response)
|
||||||
|
assert features["eps_growth_qoq"] is None
|
||||||
Loading…
Reference in New Issue