timber-common 0.3.2__tar.gz → 0.3.4__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.
- {timber_common-0.3.2 → timber_common-0.3.4}/PKG-INFO +1 -1
- timber_common-0.3.4/common/services/analytics/__init__.py +186 -0
- timber_common-0.3.4/common/services/analytics/fundamental.py +458 -0
- timber_common-0.3.4/common/services/analytics/growth.py +335 -0
- timber_common-0.3.4/common/services/analytics/valuation.py +357 -0
- timber_common-0.3.4/common/services/integrations/__init__.py +279 -0
- timber_common-0.3.4/common/services/integrations/auth_service.py +603 -0
- timber_common-0.3.4/common/services/integrations/integration_service.py +744 -0
- timber_common-0.3.4/common/services/integrations/mapping_service.py +626 -0
- timber_common-0.3.4/common/services/integrations/models.py +399 -0
- timber_common-0.3.4/common/services/integrations/registry.py +512 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/config.py +306 -1
- {timber_common-0.3.2 → timber_common-0.3.4}/pyproject.toml +1 -1
- {timber_common-0.3.2 → timber_common-0.3.4}/CHANGELOG.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/LICENSE +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/README.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/config/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/config/model_loader.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/config_executor.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/operation_registry.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/init.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/base.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/configs/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/__init.__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/tag.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/user.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/factory.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/mixins.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/models/registry.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/email_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/messaging_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/sms_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/alphavantage.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/base.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/curated_data.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/polygon.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/stock.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/yfinance.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/portfolio_metrics.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/returns.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/risk_metrics.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/standardization.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/technical_indicators.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/db_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/decision_engine.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/expression_engine.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/graph_evaluator.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/models.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/table_evaluator.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/encryption/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/encryption/field_encryption.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/context.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/core.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/ensemble.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/models.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/signals.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/gdpr/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/gdpr/deletion.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/available_capabilities.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/cached_capabilities.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/loader.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/base.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/claude_provider.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/gemini_provider.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/groq_provider.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/model_choice.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/perplexity_provider.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/DIRECTORY_STRUCTURE.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/MIGRATION_GUIDE.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/QUICKSTART.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/README.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/VIDEO_README.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/config_helpers.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/examples.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/image_generation.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/video_examples.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/video_generation.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/base.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/cache.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/instances.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/manager.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/notification.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/research.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/session.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/tracker.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/security/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/security/oauth_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/auto_ingestion.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/search.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/tag_embedding.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/plaid_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/stripe_service.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/db_utils.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/csv_parser.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/dmn_parser.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/yaml_exporter.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/yaml_parser.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/helpers.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/chunk_aggregator.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/content_condenser.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/content_handler.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/hierarchical_summarizer.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/serialization_helpers.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/time_helpers.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/validators.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/00_association_tables.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/cache_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/messaging_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/narrative_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/notification_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/oauth_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/portfolio_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/stock_research_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/user_preferences_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/data/models/vector_db_models.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/DOCUMENTATION_INDEX.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/DOCUMENTATION_SUMMARY.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/PROGRESS_UPDATE.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/TIMBER_SESSION_API_REFERENCE.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/01_model_design_patterns.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/02_service_architecture.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/03_data_fetching_strategies.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/04_caching_strategies.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/05_error_handling.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/06_performance_optimization.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/07_security_best_practices.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/01_system_architecture.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/02_config_driven_models.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/03_persistence_layer.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/04_vector_integration.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/05_multi_app_support.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/01_getting_started.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/02_creating_models.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/03_using_services.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/04_financial_data_fetching.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/05_encryption_and_security.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/06_vector_search.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/07_gdpr_compliance.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/08_testing_guide.md +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/modules/__init__.py +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/modules/config/custom_analysis.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/modules/config/investing_operations_config.yaml +0 -0
- {timber_common-0.3.2 → timber_common-0.3.4}/modules/investing_operations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timber-common
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Configuration-driven persistence library with ml tools (finance related) config driven db model registration, llm model choice, automatic encryption, caching, vector search, and GDPR compliance for Python applications
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# timber/common/services/analytics/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Financial Analytics Package
|
|
4
|
+
|
|
5
|
+
Provides fundamental analysis capabilities that are ADDITIVE to data_processor.
|
|
6
|
+
|
|
7
|
+
data_processor handles: Technical indicators, returns, risk metrics, portfolio metrics
|
|
8
|
+
analytics handles: Fundamental ratios, growth metrics, valuation (from financial statements)
|
|
9
|
+
|
|
10
|
+
Main entry points for task configs:
|
|
11
|
+
- calculate_financial_ratios(income_stmt, balance_sheet, cash_flow)
|
|
12
|
+
- calculate_growth_metrics(income_stmt, balance_sheet)
|
|
13
|
+
- calculate_valuation_metrics(income_stmt, balance_sheet, cash_flow, market_cap, ...)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .fundamental import (
|
|
17
|
+
# Individual ratio functions
|
|
18
|
+
calculate_profitability_ratios,
|
|
19
|
+
calculate_leverage_ratios,
|
|
20
|
+
calculate_liquidity_ratios,
|
|
21
|
+
calculate_efficiency_ratios,
|
|
22
|
+
calculate_cashflow_ratios,
|
|
23
|
+
# Main umbrella function
|
|
24
|
+
calculate_financial_ratios,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .growth import (
|
|
28
|
+
calculate_revenue_growth,
|
|
29
|
+
calculate_earnings_growth,
|
|
30
|
+
calculate_margin_trends,
|
|
31
|
+
calculate_asset_growth,
|
|
32
|
+
# Main umbrella function
|
|
33
|
+
calculate_growth_metrics,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from .valuation import (
|
|
37
|
+
calculate_price_multiples,
|
|
38
|
+
calculate_ev_multiples,
|
|
39
|
+
calculate_cf_valuation,
|
|
40
|
+
calculate_peg_ratio,
|
|
41
|
+
# Main umbrella function
|
|
42
|
+
calculate_valuation_metrics,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Unified Analytics Service Class
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
class FundamentalAnalytics:
|
|
51
|
+
"""
|
|
52
|
+
Unified fundamental analytics service.
|
|
53
|
+
|
|
54
|
+
Provides methods that can be called via Timber adapter:
|
|
55
|
+
service: fundamental_analytics
|
|
56
|
+
method: calculate_financial_ratios
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Fundamental Ratios
|
|
60
|
+
@staticmethod
|
|
61
|
+
def calculate_financial_ratios(income_stmt, balance_sheet, cash_flow=None, period_idx=0):
|
|
62
|
+
"""Calculate all fundamental ratios from financial statements."""
|
|
63
|
+
return calculate_financial_ratios(income_stmt, balance_sheet, cash_flow, period_idx)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def calculate_profitability_ratios(income_stmt, balance_sheet, period_idx=0):
|
|
67
|
+
"""Calculate profitability ratios."""
|
|
68
|
+
return calculate_profitability_ratios(income_stmt, balance_sheet, period_idx)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def calculate_leverage_ratios(balance_sheet, income_stmt=None, period_idx=0):
|
|
72
|
+
"""Calculate leverage/solvency ratios."""
|
|
73
|
+
return calculate_leverage_ratios(balance_sheet, income_stmt, period_idx)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def calculate_liquidity_ratios(balance_sheet, period_idx=0):
|
|
77
|
+
"""Calculate liquidity ratios."""
|
|
78
|
+
return calculate_liquidity_ratios(balance_sheet, period_idx)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def calculate_efficiency_ratios(income_stmt, balance_sheet, period_idx=0):
|
|
82
|
+
"""Calculate efficiency ratios."""
|
|
83
|
+
return calculate_efficiency_ratios(income_stmt, balance_sheet, period_idx)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def calculate_cashflow_ratios(cash_flow, income_stmt=None, balance_sheet=None, period_idx=0):
|
|
87
|
+
"""Calculate cash flow quality ratios."""
|
|
88
|
+
return calculate_cashflow_ratios(cash_flow, income_stmt, balance_sheet, period_idx)
|
|
89
|
+
|
|
90
|
+
# Growth Metrics
|
|
91
|
+
@staticmethod
|
|
92
|
+
def calculate_growth_metrics(income_stmt, balance_sheet=None):
|
|
93
|
+
"""Calculate all growth metrics."""
|
|
94
|
+
return calculate_growth_metrics(income_stmt, balance_sheet)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def calculate_revenue_growth(income_stmt):
|
|
98
|
+
"""Calculate revenue growth metrics."""
|
|
99
|
+
return calculate_revenue_growth(income_stmt)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def calculate_earnings_growth(income_stmt):
|
|
103
|
+
"""Calculate earnings growth metrics."""
|
|
104
|
+
return calculate_earnings_growth(income_stmt)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def calculate_margin_trends(income_stmt):
|
|
108
|
+
"""Calculate margin trends."""
|
|
109
|
+
return calculate_margin_trends(income_stmt)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def calculate_asset_growth(balance_sheet):
|
|
113
|
+
"""Calculate asset/equity growth."""
|
|
114
|
+
return calculate_asset_growth(balance_sheet)
|
|
115
|
+
|
|
116
|
+
# Valuation Metrics
|
|
117
|
+
@staticmethod
|
|
118
|
+
def calculate_valuation_metrics(
|
|
119
|
+
income_stmt, balance_sheet, cash_flow=None,
|
|
120
|
+
market_cap=None, current_price=None, shares_outstanding=None,
|
|
121
|
+
period_idx=0
|
|
122
|
+
):
|
|
123
|
+
"""Calculate all valuation metrics."""
|
|
124
|
+
return calculate_valuation_metrics(
|
|
125
|
+
income_stmt, balance_sheet, cash_flow,
|
|
126
|
+
market_cap, current_price, shares_outstanding,
|
|
127
|
+
period_idx
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def calculate_price_multiples(
|
|
132
|
+
income_stmt, balance_sheet, market_cap,
|
|
133
|
+
shares_outstanding=None, current_price=None, period_idx=0
|
|
134
|
+
):
|
|
135
|
+
"""Calculate P/E, P/B, P/S ratios."""
|
|
136
|
+
return calculate_price_multiples(
|
|
137
|
+
income_stmt, balance_sheet, market_cap,
|
|
138
|
+
shares_outstanding, current_price, period_idx
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def calculate_ev_multiples(income_stmt, balance_sheet, market_cap, period_idx=0):
|
|
143
|
+
"""Calculate EV/EBITDA, EV/Revenue."""
|
|
144
|
+
return calculate_ev_multiples(income_stmt, balance_sheet, market_cap, period_idx)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def calculate_peg_ratio(income_stmt, market_cap, period_idx=0):
|
|
148
|
+
"""Calculate PEG ratio."""
|
|
149
|
+
return calculate_peg_ratio(income_stmt, market_cap, period_idx)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Create singleton instance
|
|
153
|
+
fundamental_analytics = FundamentalAnalytics()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# =============================================================================
|
|
157
|
+
# Exports
|
|
158
|
+
# =============================================================================
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
# Main service object
|
|
162
|
+
'fundamental_analytics',
|
|
163
|
+
'FundamentalAnalytics',
|
|
164
|
+
|
|
165
|
+
# Fundamental Ratios
|
|
166
|
+
'calculate_financial_ratios',
|
|
167
|
+
'calculate_profitability_ratios',
|
|
168
|
+
'calculate_leverage_ratios',
|
|
169
|
+
'calculate_liquidity_ratios',
|
|
170
|
+
'calculate_efficiency_ratios',
|
|
171
|
+
'calculate_cashflow_ratios',
|
|
172
|
+
|
|
173
|
+
# Growth Metrics
|
|
174
|
+
'calculate_growth_metrics',
|
|
175
|
+
'calculate_revenue_growth',
|
|
176
|
+
'calculate_earnings_growth',
|
|
177
|
+
'calculate_margin_trends',
|
|
178
|
+
'calculate_asset_growth',
|
|
179
|
+
|
|
180
|
+
# Valuation Metrics
|
|
181
|
+
'calculate_valuation_metrics',
|
|
182
|
+
'calculate_price_multiples',
|
|
183
|
+
'calculate_ev_multiples',
|
|
184
|
+
'calculate_cf_valuation',
|
|
185
|
+
'calculate_peg_ratio',
|
|
186
|
+
]
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# timber/common/services/analytics/fundamental.py
|
|
2
|
+
"""
|
|
3
|
+
Fundamental Analysis - Financial Statement Ratios
|
|
4
|
+
|
|
5
|
+
Calculates ratios from income statement, balance sheet, and cash flow DataFrames.
|
|
6
|
+
Works with yfinance format: rows=line items, cols=dates
|
|
7
|
+
|
|
8
|
+
These are ADDITIVE to data_processor which handles price-based technical analysis.
|
|
9
|
+
|
|
10
|
+
Categories:
|
|
11
|
+
- Profitability: ROE, ROA, ROIC, margins
|
|
12
|
+
- Leverage: Debt ratios, interest coverage
|
|
13
|
+
- Liquidity: Current ratio, quick ratio
|
|
14
|
+
- Efficiency: Asset turnover, inventory turnover
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Dict, Any, Optional, Tuple, List
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# FIELD NAME MAPPINGS (handles yfinance naming variations)
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
INCOME_FIELDS = {
|
|
31
|
+
'revenue': ['Total Revenue', 'Revenue', 'Revenues', 'Sales'],
|
|
32
|
+
'gross_profit': ['Gross Profit'],
|
|
33
|
+
'operating_income': ['Operating Income', 'EBIT'],
|
|
34
|
+
'net_income': ['Net Income', 'Net Income Common Stockholders', 'Net Income From Continuing Operations'],
|
|
35
|
+
'ebitda': ['EBITDA', 'Normalized EBITDA'],
|
|
36
|
+
'interest_expense': ['Interest Expense', 'Interest Expense Non Operating'],
|
|
37
|
+
'cost_of_revenue': ['Cost Of Revenue', 'Cost of Goods Sold'],
|
|
38
|
+
'depreciation': ['Depreciation And Amortization', 'Reconciled Depreciation'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BALANCE_FIELDS = {
|
|
42
|
+
'total_assets': ['Total Assets'],
|
|
43
|
+
'total_liabilities': ['Total Liabilities Net Minority Interest', 'Total Liabilities'],
|
|
44
|
+
'total_equity': ['Stockholders Equity', 'Total Equity Gross Minority Interest', 'Common Stock Equity'],
|
|
45
|
+
'current_assets': ['Current Assets', 'Total Current Assets'],
|
|
46
|
+
'current_liabilities': ['Current Liabilities', 'Total Current Liabilities'],
|
|
47
|
+
'total_debt': ['Total Debt'],
|
|
48
|
+
'long_term_debt': ['Long Term Debt', 'Long Term Debt And Capital Lease Obligation'],
|
|
49
|
+
'short_term_debt': ['Current Debt', 'Current Debt And Capital Lease Obligation'],
|
|
50
|
+
'cash': ['Cash And Cash Equivalents', 'Cash Cash Equivalents And Short Term Investments'],
|
|
51
|
+
'inventory': ['Inventory'],
|
|
52
|
+
'receivables': ['Accounts Receivable', 'Net Receivables', 'Receivables'],
|
|
53
|
+
'payables': ['Accounts Payable', 'Payables And Accrued Expenses'],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CASHFLOW_FIELDS = {
|
|
57
|
+
'operating_cash_flow': ['Operating Cash Flow', 'Cash Flow From Continuing Operating Activities'],
|
|
58
|
+
'capital_expenditure': ['Capital Expenditure', 'Purchase Of Property Plant And Equipment'],
|
|
59
|
+
'free_cash_flow': ['Free Cash Flow'],
|
|
60
|
+
'dividends_paid': ['Cash Dividends Paid', 'Common Stock Dividend Paid', 'Payment Of Dividends'],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# VALUE EXTRACTION (yfinance format: rows=items, cols=dates)
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
def _get_value(df: pd.DataFrame, field_names: List[str], period_idx: int = 0) -> Optional[float]:
|
|
69
|
+
"""
|
|
70
|
+
Extract value from yfinance-format DataFrame.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
df: Financial statement (rows=line items, cols=dates)
|
|
74
|
+
field_names: Possible row names for the field
|
|
75
|
+
period_idx: Column index (0=most recent)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Float value or None
|
|
79
|
+
"""
|
|
80
|
+
if df is None or df.empty:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
for name in field_names:
|
|
84
|
+
if name in df.index:
|
|
85
|
+
try:
|
|
86
|
+
val = df.loc[name].iloc[period_idx]
|
|
87
|
+
if pd.notna(val):
|
|
88
|
+
return float(val)
|
|
89
|
+
except (IndexError, KeyError):
|
|
90
|
+
continue
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _safe_divide(num: Optional[float], denom: Optional[float]) -> Optional[float]:
|
|
95
|
+
"""Safely divide, returning None if invalid."""
|
|
96
|
+
if num is None or denom is None or denom == 0:
|
|
97
|
+
return None
|
|
98
|
+
return num / denom
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# PROFITABILITY RATIOS
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
def calculate_profitability_ratios(
|
|
106
|
+
income_stmt: pd.DataFrame,
|
|
107
|
+
balance_sheet: pd.DataFrame,
|
|
108
|
+
period_idx: int = 0,
|
|
109
|
+
) -> Tuple[Dict[str, Optional[float]], Optional[str]]:
|
|
110
|
+
"""
|
|
111
|
+
Calculate profitability ratios from financial statements.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
income_stmt: Income statement DataFrame
|
|
115
|
+
balance_sheet: Balance sheet DataFrame
|
|
116
|
+
period_idx: Which period (0=most recent)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tuple of (ratios_dict, error_message)
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
revenue = _get_value(income_stmt, INCOME_FIELDS['revenue'], period_idx)
|
|
123
|
+
gross_profit = _get_value(income_stmt, INCOME_FIELDS['gross_profit'], period_idx)
|
|
124
|
+
operating_income = _get_value(income_stmt, INCOME_FIELDS['operating_income'], period_idx)
|
|
125
|
+
net_income = _get_value(income_stmt, INCOME_FIELDS['net_income'], period_idx)
|
|
126
|
+
|
|
127
|
+
total_equity = _get_value(balance_sheet, BALANCE_FIELDS['total_equity'], period_idx)
|
|
128
|
+
total_assets = _get_value(balance_sheet, BALANCE_FIELDS['total_assets'], period_idx)
|
|
129
|
+
total_debt = _get_value(balance_sheet, BALANCE_FIELDS['total_debt'], period_idx)
|
|
130
|
+
|
|
131
|
+
ratios = {
|
|
132
|
+
'gross_margin': _safe_divide(gross_profit, revenue),
|
|
133
|
+
'operating_margin': _safe_divide(operating_income, revenue),
|
|
134
|
+
'net_margin': _safe_divide(net_income, revenue),
|
|
135
|
+
'roe': _safe_divide(net_income, total_equity),
|
|
136
|
+
'roa': _safe_divide(net_income, total_assets),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ROIC = NOPAT / Invested Capital (using 25% tax estimate)
|
|
140
|
+
if operating_income is not None:
|
|
141
|
+
nopat = operating_income * 0.75
|
|
142
|
+
invested_capital = (total_equity or 0) + (total_debt or 0)
|
|
143
|
+
ratios['roic'] = _safe_divide(nopat, invested_capital) if invested_capital > 0 else None
|
|
144
|
+
else:
|
|
145
|
+
ratios['roic'] = None
|
|
146
|
+
|
|
147
|
+
return ratios, None
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Error calculating profitability ratios: {e}")
|
|
151
|
+
return {}, str(e)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# =============================================================================
|
|
155
|
+
# LEVERAGE RATIOS
|
|
156
|
+
# =============================================================================
|
|
157
|
+
|
|
158
|
+
def calculate_leverage_ratios(
|
|
159
|
+
balance_sheet: pd.DataFrame,
|
|
160
|
+
income_stmt: pd.DataFrame = None,
|
|
161
|
+
period_idx: int = 0,
|
|
162
|
+
) -> Tuple[Dict[str, Optional[float]], Optional[str]]:
|
|
163
|
+
"""
|
|
164
|
+
Calculate leverage/solvency ratios.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
balance_sheet: Balance sheet DataFrame
|
|
168
|
+
income_stmt: Income statement DataFrame (optional, for interest coverage)
|
|
169
|
+
period_idx: Which period (0=most recent)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Tuple of (ratios_dict, error_message)
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
total_debt = _get_value(balance_sheet, BALANCE_FIELDS['total_debt'], period_idx)
|
|
176
|
+
long_term_debt = _get_value(balance_sheet, BALANCE_FIELDS['long_term_debt'], period_idx)
|
|
177
|
+
short_term_debt = _get_value(balance_sheet, BALANCE_FIELDS['short_term_debt'], period_idx)
|
|
178
|
+
|
|
179
|
+
# Sum components if total not available
|
|
180
|
+
if total_debt is None and (long_term_debt or short_term_debt):
|
|
181
|
+
total_debt = (long_term_debt or 0) + (short_term_debt or 0)
|
|
182
|
+
|
|
183
|
+
total_equity = _get_value(balance_sheet, BALANCE_FIELDS['total_equity'], period_idx)
|
|
184
|
+
total_assets = _get_value(balance_sheet, BALANCE_FIELDS['total_assets'], period_idx)
|
|
185
|
+
|
|
186
|
+
ratios = {
|
|
187
|
+
'debt_to_equity': _safe_divide(total_debt, total_equity),
|
|
188
|
+
'debt_to_assets': _safe_divide(total_debt, total_assets),
|
|
189
|
+
'equity_multiplier': _safe_divide(total_assets, total_equity),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Interest coverage requires income statement
|
|
193
|
+
if income_stmt is not None and not income_stmt.empty:
|
|
194
|
+
operating_income = _get_value(income_stmt, INCOME_FIELDS['operating_income'], period_idx)
|
|
195
|
+
interest_expense = _get_value(income_stmt, INCOME_FIELDS['interest_expense'], period_idx)
|
|
196
|
+
depreciation = _get_value(income_stmt, INCOME_FIELDS['depreciation'], period_idx)
|
|
197
|
+
|
|
198
|
+
if interest_expense and interest_expense > 0:
|
|
199
|
+
ratios['interest_coverage'] = _safe_divide(operating_income, interest_expense)
|
|
200
|
+
else:
|
|
201
|
+
ratios['interest_coverage'] = None
|
|
202
|
+
|
|
203
|
+
# Debt to EBITDA
|
|
204
|
+
if operating_income is not None:
|
|
205
|
+
ebitda = operating_income + (depreciation or 0)
|
|
206
|
+
ratios['debt_to_ebitda'] = _safe_divide(total_debt, ebitda)
|
|
207
|
+
else:
|
|
208
|
+
ratios['debt_to_ebitda'] = None
|
|
209
|
+
|
|
210
|
+
return ratios, None
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Error calculating leverage ratios: {e}")
|
|
214
|
+
return {}, str(e)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# =============================================================================
|
|
218
|
+
# LIQUIDITY RATIOS
|
|
219
|
+
# =============================================================================
|
|
220
|
+
|
|
221
|
+
def calculate_liquidity_ratios(
|
|
222
|
+
balance_sheet: pd.DataFrame,
|
|
223
|
+
period_idx: int = 0,
|
|
224
|
+
) -> Tuple[Dict[str, Optional[float]], Optional[str]]:
|
|
225
|
+
"""
|
|
226
|
+
Calculate liquidity ratios.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
balance_sheet: Balance sheet DataFrame
|
|
230
|
+
period_idx: Which period (0=most recent)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Tuple of (ratios_dict, error_message)
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
current_assets = _get_value(balance_sheet, BALANCE_FIELDS['current_assets'], period_idx)
|
|
237
|
+
current_liabilities = _get_value(balance_sheet, BALANCE_FIELDS['current_liabilities'], period_idx)
|
|
238
|
+
inventory = _get_value(balance_sheet, BALANCE_FIELDS['inventory'], period_idx)
|
|
239
|
+
cash = _get_value(balance_sheet, BALANCE_FIELDS['cash'], period_idx)
|
|
240
|
+
|
|
241
|
+
ratios = {
|
|
242
|
+
'current_ratio': _safe_divide(current_assets, current_liabilities),
|
|
243
|
+
'cash_ratio': _safe_divide(cash, current_liabilities),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Quick ratio = (Current Assets - Inventory) / Current Liabilities
|
|
247
|
+
if current_assets is not None:
|
|
248
|
+
quick_assets = current_assets - (inventory or 0)
|
|
249
|
+
ratios['quick_ratio'] = _safe_divide(quick_assets, current_liabilities)
|
|
250
|
+
else:
|
|
251
|
+
ratios['quick_ratio'] = None
|
|
252
|
+
|
|
253
|
+
# Working capital
|
|
254
|
+
if current_assets is not None and current_liabilities is not None:
|
|
255
|
+
ratios['working_capital'] = current_assets - current_liabilities
|
|
256
|
+
else:
|
|
257
|
+
ratios['working_capital'] = None
|
|
258
|
+
|
|
259
|
+
return ratios, None
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Error calculating liquidity ratios: {e}")
|
|
263
|
+
return {}, str(e)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# EFFICIENCY RATIOS
|
|
268
|
+
# =============================================================================
|
|
269
|
+
|
|
270
|
+
def calculate_efficiency_ratios(
|
|
271
|
+
income_stmt: pd.DataFrame,
|
|
272
|
+
balance_sheet: pd.DataFrame,
|
|
273
|
+
period_idx: int = 0,
|
|
274
|
+
) -> Tuple[Dict[str, Optional[float]], Optional[str]]:
|
|
275
|
+
"""
|
|
276
|
+
Calculate efficiency/activity ratios.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
income_stmt: Income statement DataFrame
|
|
280
|
+
balance_sheet: Balance sheet DataFrame
|
|
281
|
+
period_idx: Which period (0=most recent)
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Tuple of (ratios_dict, error_message)
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
revenue = _get_value(income_stmt, INCOME_FIELDS['revenue'], period_idx)
|
|
288
|
+
cogs = _get_value(income_stmt, INCOME_FIELDS['cost_of_revenue'], period_idx)
|
|
289
|
+
|
|
290
|
+
total_assets = _get_value(balance_sheet, BALANCE_FIELDS['total_assets'], period_idx)
|
|
291
|
+
inventory = _get_value(balance_sheet, BALANCE_FIELDS['inventory'], period_idx)
|
|
292
|
+
receivables = _get_value(balance_sheet, BALANCE_FIELDS['receivables'], period_idx)
|
|
293
|
+
payables = _get_value(balance_sheet, BALANCE_FIELDS['payables'], period_idx)
|
|
294
|
+
|
|
295
|
+
ratios = {
|
|
296
|
+
'asset_turnover': _safe_divide(revenue, total_assets),
|
|
297
|
+
'inventory_turnover': _safe_divide(cogs, inventory),
|
|
298
|
+
'receivables_turnover': _safe_divide(revenue, receivables),
|
|
299
|
+
'payables_turnover': _safe_divide(cogs, payables),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Days calculations (annual figures)
|
|
303
|
+
ratios['days_inventory'] = _safe_divide(365, ratios['inventory_turnover'])
|
|
304
|
+
ratios['days_receivables'] = _safe_divide(365, ratios['receivables_turnover'])
|
|
305
|
+
ratios['days_payables'] = _safe_divide(365, ratios['payables_turnover'])
|
|
306
|
+
|
|
307
|
+
# Cash Conversion Cycle = DIO + DSO - DPO
|
|
308
|
+
dio = ratios.get('days_inventory')
|
|
309
|
+
dso = ratios.get('days_receivables')
|
|
310
|
+
dpo = ratios.get('days_payables')
|
|
311
|
+
|
|
312
|
+
if all(v is not None for v in [dio, dso, dpo]):
|
|
313
|
+
ratios['cash_conversion_cycle'] = dio + dso - dpo
|
|
314
|
+
else:
|
|
315
|
+
ratios['cash_conversion_cycle'] = None
|
|
316
|
+
|
|
317
|
+
return ratios, None
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.error(f"Error calculating efficiency ratios: {e}")
|
|
321
|
+
return {}, str(e)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# =============================================================================
|
|
325
|
+
# CASH FLOW RATIOS
|
|
326
|
+
# =============================================================================
|
|
327
|
+
|
|
328
|
+
def calculate_cashflow_ratios(
|
|
329
|
+
cash_flow: pd.DataFrame,
|
|
330
|
+
income_stmt: pd.DataFrame = None,
|
|
331
|
+
balance_sheet: pd.DataFrame = None,
|
|
332
|
+
period_idx: int = 0,
|
|
333
|
+
) -> Tuple[Dict[str, Optional[float]], Optional[str]]:
|
|
334
|
+
"""
|
|
335
|
+
Calculate cash flow quality ratios.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
cash_flow: Cash flow statement DataFrame
|
|
339
|
+
income_stmt: Income statement DataFrame (optional)
|
|
340
|
+
balance_sheet: Balance sheet DataFrame (optional)
|
|
341
|
+
period_idx: Which period (0=most recent)
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Tuple of (ratios_dict, error_message)
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
ocf = _get_value(cash_flow, CASHFLOW_FIELDS['operating_cash_flow'], period_idx)
|
|
348
|
+
capex = _get_value(cash_flow, CASHFLOW_FIELDS['capital_expenditure'], period_idx)
|
|
349
|
+
fcf = _get_value(cash_flow, CASHFLOW_FIELDS['free_cash_flow'], period_idx)
|
|
350
|
+
|
|
351
|
+
# Calculate FCF if not directly available
|
|
352
|
+
if fcf is None and ocf is not None and capex is not None:
|
|
353
|
+
fcf = ocf + capex # capex is typically negative
|
|
354
|
+
|
|
355
|
+
ratios = {
|
|
356
|
+
'operating_cash_flow': ocf,
|
|
357
|
+
'free_cash_flow': fcf,
|
|
358
|
+
'capex': capex,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Cash flow to net income ratio (earnings quality)
|
|
362
|
+
if income_stmt is not None and not income_stmt.empty:
|
|
363
|
+
net_income = _get_value(income_stmt, INCOME_FIELDS['net_income'], period_idx)
|
|
364
|
+
ratios['ocf_to_net_income'] = _safe_divide(ocf, net_income)
|
|
365
|
+
|
|
366
|
+
# FCF to revenue
|
|
367
|
+
if income_stmt is not None and not income_stmt.empty:
|
|
368
|
+
revenue = _get_value(income_stmt, INCOME_FIELDS['revenue'], period_idx)
|
|
369
|
+
ratios['fcf_margin'] = _safe_divide(fcf, revenue)
|
|
370
|
+
|
|
371
|
+
# FCF to debt (debt coverage)
|
|
372
|
+
if balance_sheet is not None and not balance_sheet.empty:
|
|
373
|
+
total_debt = _get_value(balance_sheet, BALANCE_FIELDS['total_debt'], period_idx)
|
|
374
|
+
ratios['fcf_to_debt'] = _safe_divide(fcf, total_debt)
|
|
375
|
+
|
|
376
|
+
return ratios, None
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"Error calculating cash flow ratios: {e}")
|
|
380
|
+
return {}, str(e)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =============================================================================
|
|
384
|
+
# UMBRELLA FUNCTION - Main Entry Point
|
|
385
|
+
# =============================================================================
|
|
386
|
+
|
|
387
|
+
def calculate_financial_ratios(
|
|
388
|
+
income_stmt: pd.DataFrame,
|
|
389
|
+
balance_sheet: pd.DataFrame,
|
|
390
|
+
cash_flow: pd.DataFrame = None,
|
|
391
|
+
period_idx: int = 0,
|
|
392
|
+
) -> Tuple[Dict[str, Any], Optional[str]]:
|
|
393
|
+
"""
|
|
394
|
+
Calculate all fundamental ratios from financial statements.
|
|
395
|
+
|
|
396
|
+
This is the main entry point called by task configs via:
|
|
397
|
+
service: analytics_service
|
|
398
|
+
method: calculate_financial_ratios
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
income_stmt: Income statement DataFrame (yfinance format)
|
|
402
|
+
balance_sheet: Balance sheet DataFrame (yfinance format)
|
|
403
|
+
cash_flow: Cash flow statement DataFrame (optional)
|
|
404
|
+
period_idx: Which period to analyze (0=most recent)
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Tuple of (result_dict, error_string or None)
|
|
408
|
+
"""
|
|
409
|
+
try:
|
|
410
|
+
result = {
|
|
411
|
+
'success': True,
|
|
412
|
+
'period_idx': period_idx,
|
|
413
|
+
'calculated_at': datetime.now(timezone.utc).isoformat(),
|
|
414
|
+
'errors': {},
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Get period date if available
|
|
418
|
+
if not income_stmt.empty and len(income_stmt.columns) > period_idx:
|
|
419
|
+
result['period_date'] = str(income_stmt.columns[period_idx])
|
|
420
|
+
|
|
421
|
+
# Calculate each category
|
|
422
|
+
profitability, err = calculate_profitability_ratios(income_stmt, balance_sheet, period_idx)
|
|
423
|
+
if err:
|
|
424
|
+
result['errors']['profitability'] = err
|
|
425
|
+
result['profitability'] = profitability
|
|
426
|
+
|
|
427
|
+
leverage, err = calculate_leverage_ratios(balance_sheet, income_stmt, period_idx)
|
|
428
|
+
if err:
|
|
429
|
+
result['errors']['leverage'] = err
|
|
430
|
+
result['leverage'] = leverage
|
|
431
|
+
|
|
432
|
+
liquidity, err = calculate_liquidity_ratios(balance_sheet, period_idx)
|
|
433
|
+
if err:
|
|
434
|
+
result['errors']['liquidity'] = err
|
|
435
|
+
result['liquidity'] = liquidity
|
|
436
|
+
|
|
437
|
+
efficiency, err = calculate_efficiency_ratios(income_stmt, balance_sheet, period_idx)
|
|
438
|
+
if err:
|
|
439
|
+
result['errors']['efficiency'] = err
|
|
440
|
+
result['efficiency'] = efficiency
|
|
441
|
+
|
|
442
|
+
if cash_flow is not None and not cash_flow.empty:
|
|
443
|
+
cashflow_ratios, err = calculate_cashflow_ratios(
|
|
444
|
+
cash_flow, income_stmt, balance_sheet, period_idx
|
|
445
|
+
)
|
|
446
|
+
if err:
|
|
447
|
+
result['errors']['cash_flow'] = err
|
|
448
|
+
result['cash_flow'] = cashflow_ratios
|
|
449
|
+
|
|
450
|
+
# Mark as failed if all categories errored
|
|
451
|
+
if len(result['errors']) >= 4:
|
|
452
|
+
result['success'] = False
|
|
453
|
+
|
|
454
|
+
return result, None
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(f"Error in calculate_financial_ratios: {e}")
|
|
458
|
+
return {'success': False, 'error': str(e)}, str(e)
|