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.
Files changed (162) hide show
  1. {timber_common-0.3.2 → timber_common-0.3.4}/PKG-INFO +1 -1
  2. timber_common-0.3.4/common/services/analytics/__init__.py +186 -0
  3. timber_common-0.3.4/common/services/analytics/fundamental.py +458 -0
  4. timber_common-0.3.4/common/services/analytics/growth.py +335 -0
  5. timber_common-0.3.4/common/services/analytics/valuation.py +357 -0
  6. timber_common-0.3.4/common/services/integrations/__init__.py +279 -0
  7. timber_common-0.3.4/common/services/integrations/auth_service.py +603 -0
  8. timber_common-0.3.4/common/services/integrations/integration_service.py +744 -0
  9. timber_common-0.3.4/common/services/integrations/mapping_service.py +626 -0
  10. timber_common-0.3.4/common/services/integrations/models.py +399 -0
  11. timber_common-0.3.4/common/services/integrations/registry.py +512 -0
  12. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/config.py +306 -1
  13. {timber_common-0.3.2 → timber_common-0.3.4}/pyproject.toml +1 -1
  14. {timber_common-0.3.2 → timber_common-0.3.4}/CHANGELOG.md +0 -0
  15. {timber_common-0.3.2 → timber_common-0.3.4}/LICENSE +0 -0
  16. {timber_common-0.3.2 → timber_common-0.3.4}/README.md +0 -0
  17. {timber_common-0.3.2 → timber_common-0.3.4}/common/__init__.py +0 -0
  18. {timber_common-0.3.2 → timber_common-0.3.4}/common/config/__init__.py +0 -0
  19. {timber_common-0.3.2 → timber_common-0.3.4}/common/config/model_loader.py +0 -0
  20. {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/__init__.py +0 -0
  21. {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/config_executor.py +0 -0
  22. {timber_common-0.3.2 → timber_common-0.3.4}/common/engine/operation_registry.py +0 -0
  23. {timber_common-0.3.2 → timber_common-0.3.4}/common/init.py +0 -0
  24. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/__init__.py +0 -0
  25. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/base.py +0 -0
  26. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/configs/__init__.py +0 -0
  27. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/__init.__.py +0 -0
  28. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/tag.py +0 -0
  29. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/core/user.py +0 -0
  30. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/factory.py +0 -0
  31. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/mixins.py +0 -0
  32. {timber_common-0.3.2 → timber_common-0.3.4}/common/models/registry.py +0 -0
  33. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/__init__.py +0 -0
  34. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/__init__.py +0 -0
  35. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/email_service.py +0 -0
  36. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/messaging_service.py +0 -0
  37. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/communication/sms_service.py +0 -0
  38. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/__init__.py +0 -0
  39. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/alphavantage.py +0 -0
  40. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/base.py +0 -0
  41. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/curated_data.py +0 -0
  42. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/polygon.py +0 -0
  43. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/stock.py +0 -0
  44. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_fetcher/yfinance.py +0 -0
  45. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/__init__.py +0 -0
  46. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/portfolio_metrics.py +0 -0
  47. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/returns.py +0 -0
  48. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/risk_metrics.py +0 -0
  49. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/standardization.py +0 -0
  50. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/data_processor/technical_indicators.py +0 -0
  51. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/db_service.py +0 -0
  52. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/__init__.py +0 -0
  53. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/decision_engine.py +0 -0
  54. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/expression_engine.py +0 -0
  55. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/graph_evaluator.py +0 -0
  56. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/models.py +0 -0
  57. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/decisioning/table_evaluator.py +0 -0
  58. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/encryption/__init__.py +0 -0
  59. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/encryption/field_encryption.py +0 -0
  60. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/__init__.py +0 -0
  61. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/context.py +0 -0
  62. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/core.py +0 -0
  63. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/ensemble.py +0 -0
  64. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/models.py +0 -0
  65. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/service.py +0 -0
  66. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/forecasting/signals.py +0 -0
  67. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/gdpr/__init__.py +0 -0
  68. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/gdpr/deletion.py +0 -0
  69. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/__init__.py +0 -0
  70. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/available_capabilities.py +0 -0
  71. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/cached_capabilities.py +0 -0
  72. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/inventory/loader.py +0 -0
  73. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/__init__.py +0 -0
  74. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/base.py +0 -0
  75. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/claude_provider.py +0 -0
  76. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/gemini_provider.py +0 -0
  77. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/groq_provider.py +0 -0
  78. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/model_choice.py +0 -0
  79. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm/perplexity_provider.py +0 -0
  80. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/llm_service.py +0 -0
  81. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/DIRECTORY_STRUCTURE.md +0 -0
  82. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/MIGRATION_GUIDE.md +0 -0
  83. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/QUICKSTART.md +0 -0
  84. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/README.md +0 -0
  85. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/VIDEO_README.md +0 -0
  86. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/__init__.py +0 -0
  87. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/config_helpers.py +0 -0
  88. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/examples.py +0 -0
  89. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/image_generation.py +0 -0
  90. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/video_examples.py +0 -0
  91. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/media/video_generation.py +0 -0
  92. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/__init__.py +0 -0
  93. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/base.py +0 -0
  94. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/cache.py +0 -0
  95. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/instances.py +0 -0
  96. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/manager.py +0 -0
  97. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/notification.py +0 -0
  98. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/research.py +0 -0
  99. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/session.py +0 -0
  100. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/persistence/tracker.py +0 -0
  101. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/security/__init__.py +0 -0
  102. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/security/oauth_service.py +0 -0
  103. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/__init__.py +0 -0
  104. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/auto_ingestion.py +0 -0
  105. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/search.py +0 -0
  106. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vector/tag_embedding.py +0 -0
  107. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/__init__.py +0 -0
  108. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/plaid_service.py +0 -0
  109. {timber_common-0.3.2 → timber_common-0.3.4}/common/services/vendors/stripe_service.py +0 -0
  110. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/__init__.py +0 -0
  111. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/db_utils.py +0 -0
  112. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/__init__.py +0 -0
  113. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/csv_parser.py +0 -0
  114. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/dmn_parser.py +0 -0
  115. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/yaml_exporter.py +0 -0
  116. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/decisioning/yaml_parser.py +0 -0
  117. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/helpers.py +0 -0
  118. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/chunk_aggregator.py +0 -0
  119. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/content_condenser.py +0 -0
  120. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/content_handler.py +0 -0
  121. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/llm/hierarchical_summarizer.py +0 -0
  122. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/serialization_helpers.py +0 -0
  123. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/time_helpers.py +0 -0
  124. {timber_common-0.3.2 → timber_common-0.3.4}/common/utils/validators.py +0 -0
  125. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/00_association_tables.yaml +0 -0
  126. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/cache_models.yaml +0 -0
  127. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/messaging_models.yaml +0 -0
  128. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/narrative_models.yaml +0 -0
  129. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/notification_models.yaml +0 -0
  130. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/oauth_models.yaml +0 -0
  131. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/portfolio_models.yaml +0 -0
  132. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/stock_research_models.yaml +0 -0
  133. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/user_preferences_models.yaml +0 -0
  134. {timber_common-0.3.2 → timber_common-0.3.4}/data/models/vector_db_models.yaml +0 -0
  135. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/DOCUMENTATION_INDEX.md +0 -0
  136. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/DOCUMENTATION_SUMMARY.md +0 -0
  137. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/PROGRESS_UPDATE.md +0 -0
  138. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/TIMBER_SESSION_API_REFERENCE.md +0 -0
  139. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/01_model_design_patterns.md +0 -0
  140. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/02_service_architecture.md +0 -0
  141. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/03_data_fetching_strategies.md +0 -0
  142. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/04_caching_strategies.md +0 -0
  143. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/05_error_handling.md +0 -0
  144. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/06_performance_optimization.md +0 -0
  145. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/best_practices/07_security_best_practices.md +0 -0
  146. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/01_system_architecture.md +0 -0
  147. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/02_config_driven_models.md +0 -0
  148. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/03_persistence_layer.md +0 -0
  149. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/04_vector_integration.md +0 -0
  150. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/design_guides/05_multi_app_support.md +0 -0
  151. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/01_getting_started.md +0 -0
  152. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/02_creating_models.md +0 -0
  153. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/03_using_services.md +0 -0
  154. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/04_financial_data_fetching.md +0 -0
  155. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/05_encryption_and_security.md +0 -0
  156. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/06_vector_search.md +0 -0
  157. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/07_gdpr_compliance.md +0 -0
  158. {timber_common-0.3.2 → timber_common-0.3.4}/documentation/how_to/08_testing_guide.md +0 -0
  159. {timber_common-0.3.2 → timber_common-0.3.4}/modules/__init__.py +0 -0
  160. {timber_common-0.3.2 → timber_common-0.3.4}/modules/config/custom_analysis.yaml +0 -0
  161. {timber_common-0.3.2 → timber_common-0.3.4}/modules/config/investing_operations_config.yaml +0 -0
  162. {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.2
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)