fin-infra 0.6.0__tar.gz → 0.8.0__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.
- {fin_infra-0.6.0 → fin_infra-0.8.0}/PKG-INFO +7 -1
- {fin_infra-0.6.0 → fin_infra-0.8.0}/pyproject.toml +8 -1
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/__init__.py +24 -0
- fin_infra-0.8.0/src/fin_infra/analytics/benchmark.py +594 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/ease.py +33 -2
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/models.py +3 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/portfolio.py +113 -23
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/rebalancing.py +50 -4
- fin_infra-0.8.0/src/fin_infra/analytics/rebalancing_llm.py +710 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/banking/__init__.py +4 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/llm_layer.py +1 -1
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/insights/__init__.py +2 -1
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/insights/aggregator.py +106 -45
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/brokerage.py +1 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/banking/teller_client.py +43 -8
- {fin_infra-0.6.0 → fin_infra-0.8.0}/LICENSE +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/README.md +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/__main__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/cash_flow.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/projections.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/savings.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/scenarios.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/analytics/spending.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/banking/history.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/banking/utils.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/brokerage/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/alerts.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/scaffold_templates/README.md +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/templates.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/budgets/tracker.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/cashflows/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/cashflows/core.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/engine.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/rules.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/categorization/taxonomy.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/chat/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/chat/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/chat/planning.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/cli/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/cli/cmds/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/cli/cmds/scaffold_cmds.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/clients/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/clients/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/clients/plaid.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/compliance/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/experian/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/experian/auth.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/experian/client.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/experian/parser.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/experian/provider.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/credit/mock.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/crypto/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/crypto/insights.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/analysis.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/ocr.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/documents/storage.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/exceptions.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/funding.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/management.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/milestones.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/scaffold_templates/README.md +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/goals/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/insights/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/providers/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/providers/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/providers/plaid.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/providers/snaptrade.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/scaffold_templates/README.md +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/investments/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/markets/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/accounts.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/candle.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/credit.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/money.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/quotes.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/tax.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/models/transactions.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/aggregator.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/calculator.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/goals.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/insights.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/scaffold_templates/README.md +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/net_worth/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/currency_converter.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/providers/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/providers/exchangerate.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/providers/static_mappings.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/normalization/symbol_resolver.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/obs/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/obs/classifier.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/banking/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/banking/plaid_client.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/brokerage/alpaca.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/brokerage/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/credit/experian.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/identity/stripe_identity.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/market/alphavantage.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/market/base.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/market/ccxt_crypto.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/market/coingecko.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/market/yahoo.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/registry.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/tax/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/tax/irs.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/tax/mock.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/providers/tax/taxbit.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/py.typed +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/detector.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/detectors_llm.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/ease.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/insights.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/normalizer.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/normalizers.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/recurring/summary.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/scaffold/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/scaffold/budgets.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/scaffold/goals.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/audit.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/encryption.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/models.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/pii_filter.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/pii_patterns.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/security/token_store.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/settings.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/tax/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/tax/add.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/tax/tlh.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/utils/__init__.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/utils/deprecation.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/utils/http.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/utils/retry.py +0 -0
- {fin_infra-0.6.0 → fin_infra-0.8.0}/src/fin_infra/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fin-infra
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: finance,banking,plaid,brokerage,markets,credit,tax,cashflow,fintech,infra
|
|
@@ -26,20 +26,26 @@ Provides-Extra: markets
|
|
|
26
26
|
Provides-Extra: plaid
|
|
27
27
|
Provides-Extra: yahoo
|
|
28
28
|
Requires-Dist: ai-infra (>=0.1.142)
|
|
29
|
+
Requires-Dist: authlib (>=1.6.6)
|
|
29
30
|
Requires-Dist: cashews[redis] (>=7.0)
|
|
30
31
|
Requires-Dist: ccxt (>=4.0.0) ; extra == "markets" or extra == "crypto" or extra == "all"
|
|
31
32
|
Requires-Dist: fastapi-users (>=15.0.2,<16.0.0)
|
|
33
|
+
Requires-Dist: filelock (>=3.20.3)
|
|
32
34
|
Requires-Dist: httpx (>=0.25.0)
|
|
33
35
|
Requires-Dist: langchain-core (>=1.2.5,<2.0.0)
|
|
34
36
|
Requires-Dist: loguru (>=0.7.0)
|
|
35
37
|
Requires-Dist: numpy (>=1.24.0)
|
|
36
38
|
Requires-Dist: numpy-financial (>=1.0.0)
|
|
37
39
|
Requires-Dist: plaid-python (>=25.0.0) ; extra == "plaid" or extra == "banking" or extra == "all"
|
|
40
|
+
Requires-Dist: protobuf (>=6.33.3)
|
|
38
41
|
Requires-Dist: pydantic (>=2.0)
|
|
39
42
|
Requires-Dist: pydantic-settings (>=2.0)
|
|
40
43
|
Requires-Dist: python-dotenv (>=1.0.0)
|
|
41
44
|
Requires-Dist: tenacity (>=8.0.0)
|
|
42
45
|
Requires-Dist: typing-extensions (>=4.0)
|
|
46
|
+
Requires-Dist: urllib3 (>=2.6.3)
|
|
47
|
+
Requires-Dist: virtualenv (>=20.36.1)
|
|
48
|
+
Requires-Dist: werkzeug (>=3.1.5)
|
|
43
49
|
Requires-Dist: yahooquery (>=2.3.0) ; extra == "markets" or extra == "yahoo" or extra == "all"
|
|
44
50
|
Project-URL: Documentation, https://nfrax.com/fin-infra
|
|
45
51
|
Project-URL: Homepage, https://github.com/nfraxlab/fin-infra
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "fin-infra"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -48,6 +48,13 @@ cashews = { version = ">=7.0", extras = ["redis"] }
|
|
|
48
48
|
loguru = ">=0.7.0"
|
|
49
49
|
numpy = ">=1.24.0"
|
|
50
50
|
numpy-financial = ">=1.0.0"
|
|
51
|
+
# Security fixes for CVEs
|
|
52
|
+
protobuf = ">=6.33.3" # CVE-2026-0994
|
|
53
|
+
authlib = ">=1.6.6" # CVE-2025-68158
|
|
54
|
+
filelock = ">=3.20.3" # CVE-2026-22701
|
|
55
|
+
urllib3 = ">=2.6.3" # CVE-2026-21441
|
|
56
|
+
virtualenv = ">=20.36.1" # CVE-2026-22702
|
|
57
|
+
werkzeug = ">=3.1.5" # CVE-2026-21860
|
|
51
58
|
# svc-infra = { path = "../svc-infra", develop = true } # Local dev only - uncomment for local development
|
|
52
59
|
|
|
53
60
|
# ai-infra for LLM-powered features (always included)
|
|
@@ -57,11 +57,35 @@ from __future__ import annotations
|
|
|
57
57
|
|
|
58
58
|
from .add import add_analytics
|
|
59
59
|
|
|
60
|
+
# Import benchmark functions for direct access
|
|
61
|
+
from .benchmark import (
|
|
62
|
+
COMMON_BENCHMARKS,
|
|
63
|
+
BenchmarkDataPoint,
|
|
64
|
+
BenchmarkHistory,
|
|
65
|
+
PortfolioVsBenchmark,
|
|
66
|
+
compare_portfolio_to_benchmark,
|
|
67
|
+
get_benchmark_history,
|
|
68
|
+
is_common_benchmark,
|
|
69
|
+
list_common_benchmarks,
|
|
70
|
+
)
|
|
71
|
+
|
|
60
72
|
# Import actual implementations
|
|
61
73
|
from .ease import AnalyticsEngine, easy_analytics
|
|
62
74
|
|
|
63
75
|
__all__ = [
|
|
76
|
+
# Easy setup
|
|
64
77
|
"easy_analytics",
|
|
65
78
|
"add_analytics",
|
|
66
79
|
"AnalyticsEngine",
|
|
80
|
+
# Benchmark functions (real market data - accepts ANY ticker)
|
|
81
|
+
"get_benchmark_history",
|
|
82
|
+
"compare_portfolio_to_benchmark",
|
|
83
|
+
# Reference list of common benchmarks (not a restriction)
|
|
84
|
+
"COMMON_BENCHMARKS",
|
|
85
|
+
"list_common_benchmarks",
|
|
86
|
+
"is_common_benchmark",
|
|
87
|
+
# Benchmark models
|
|
88
|
+
"BenchmarkHistory",
|
|
89
|
+
"BenchmarkDataPoint",
|
|
90
|
+
"PortfolioVsBenchmark",
|
|
67
91
|
]
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""Benchmark comparison and historical performance analysis.
|
|
2
|
+
|
|
3
|
+
Provides real market data integration for portfolio vs benchmark comparisons.
|
|
4
|
+
Uses fin-infra's market data providers (easy_market) for historical prices.
|
|
5
|
+
|
|
6
|
+
Generic Applicability:
|
|
7
|
+
- Personal finance apps: Portfolio performance tracking
|
|
8
|
+
- Wealth management: Client reporting and benchmarking
|
|
9
|
+
- Robo-advisors: Automated performance attribution
|
|
10
|
+
- Investment platforms: Historical chart data
|
|
11
|
+
- Financial advisors: Performance comparison reports
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
- Real historical prices from market data providers (Yahoo Finance, Alpha Vantage)
|
|
15
|
+
- Time-series data for charting (normalized to 100)
|
|
16
|
+
- Alpha, beta, and Sharpe ratio calculations
|
|
17
|
+
- Multi-benchmark support (SPY, QQQ, VTI, BND, custom)
|
|
18
|
+
- Caching-friendly design (keyword-only arguments)
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
>>> from fin_infra.analytics.benchmark import (
|
|
22
|
+
... get_benchmark_history,
|
|
23
|
+
... compare_portfolio_to_benchmark,
|
|
24
|
+
... )
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Get historical benchmark data for charting
|
|
27
|
+
>>> history = await get_benchmark_history("SPY", period="1y")
|
|
28
|
+
>>> print(f"SPY 1Y return: {history.total_return_percent:.2f}%")
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Compare portfolio to benchmark
|
|
31
|
+
>>> comparison = await compare_portfolio_to_benchmark(
|
|
32
|
+
... portfolio_history=[...], # List of portfolio snapshots
|
|
33
|
+
... benchmark="SPY",
|
|
34
|
+
... period="1y",
|
|
35
|
+
... )
|
|
36
|
+
>>> print(f"Alpha: {comparison.alpha:.2f}%")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import datetime as dt
|
|
42
|
+
import logging
|
|
43
|
+
from collections.abc import Sequence
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from ..providers.base import MarketDataProvider
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ============================================================================
|
|
55
|
+
# Models
|
|
56
|
+
# ============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BenchmarkDataPoint(BaseModel):
|
|
60
|
+
"""Single data point for benchmark time series."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
date: dt.date = Field(..., description="Date of the data point")
|
|
65
|
+
close: float = Field(..., description="Closing price")
|
|
66
|
+
normalized: float = Field(..., description="Normalized value (starting at 100)")
|
|
67
|
+
return_pct: float = Field(..., description="Return percentage from start")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BenchmarkHistory(BaseModel):
|
|
71
|
+
"""Historical benchmark data for a given period.
|
|
72
|
+
|
|
73
|
+
Designed for chart visualization with normalized values starting at 100.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(extra="forbid")
|
|
77
|
+
|
|
78
|
+
symbol: str = Field(..., description="Benchmark ticker symbol (e.g., SPY)")
|
|
79
|
+
period: str = Field(..., description="Time period (1m, 3m, 6m, 1y, ytd, all)")
|
|
80
|
+
start_date: dt.date = Field(..., description="Start date of the period")
|
|
81
|
+
end_date: dt.date = Field(..., description="End date of the period")
|
|
82
|
+
data_points: list[BenchmarkDataPoint] = Field(
|
|
83
|
+
default_factory=list, description="Time series data points"
|
|
84
|
+
)
|
|
85
|
+
start_price: float = Field(..., description="Starting price")
|
|
86
|
+
end_price: float = Field(..., description="Ending price")
|
|
87
|
+
total_return_percent: float = Field(..., description="Total return for period (%)")
|
|
88
|
+
annualized_return_percent: float | None = Field(None, description="Annualized return (%)")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PortfolioVsBenchmark(BaseModel):
|
|
92
|
+
"""Complete portfolio vs benchmark comparison with time series.
|
|
93
|
+
|
|
94
|
+
Provides all data needed for performance comparison charts and summaries.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid")
|
|
98
|
+
|
|
99
|
+
benchmark_symbol: str = Field(..., description="Benchmark ticker symbol")
|
|
100
|
+
period: str = Field(..., description="Comparison period")
|
|
101
|
+
start_date: dt.date = Field(..., description="Start date")
|
|
102
|
+
end_date: dt.date = Field(..., description="End date")
|
|
103
|
+
|
|
104
|
+
# Summary metrics
|
|
105
|
+
portfolio_return_percent: float = Field(..., description="Portfolio total return (%)")
|
|
106
|
+
benchmark_return_percent: float = Field(..., description="Benchmark total return (%)")
|
|
107
|
+
alpha: float = Field(..., description="Excess return vs benchmark (%)")
|
|
108
|
+
beta: float | None = Field(None, description="Portfolio beta vs benchmark")
|
|
109
|
+
sharpe_ratio: float | None = Field(None, description="Portfolio Sharpe ratio")
|
|
110
|
+
|
|
111
|
+
# Time series for charting
|
|
112
|
+
portfolio_series: list[BenchmarkDataPoint] = Field(
|
|
113
|
+
default_factory=list, description="Portfolio normalized time series"
|
|
114
|
+
)
|
|
115
|
+
benchmark_series: list[BenchmarkDataPoint] = Field(
|
|
116
|
+
default_factory=list, description="Benchmark normalized time series"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Common Benchmarks (Reference Only)
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
# This is a reference list of commonly used benchmarks.
|
|
125
|
+
# fin-infra does NOT restrict which tickers can be used - any valid ticker works.
|
|
126
|
+
# Application layers (fin-api, fin-web) should define their own allowed lists.
|
|
127
|
+
COMMON_BENCHMARKS = {
|
|
128
|
+
"SPY": "S&P 500 (SPDR)",
|
|
129
|
+
"QQQ": "Nasdaq 100 (Invesco)",
|
|
130
|
+
"VTI": "Total US Stock Market (Vanguard)",
|
|
131
|
+
"BND": "Total Bond Market (Vanguard)",
|
|
132
|
+
"VT": "Total World Stock (Vanguard)",
|
|
133
|
+
"AGG": "US Aggregate Bond (iShares)",
|
|
134
|
+
"IWM": "Russell 2000 (iShares)",
|
|
135
|
+
"EFA": "EAFE International (iShares)",
|
|
136
|
+
"VNQ": "Real Estate (Vanguard)",
|
|
137
|
+
"GLD": "Gold (SPDR)",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ============================================================================
|
|
142
|
+
# Period Parsing
|
|
143
|
+
# ============================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_period_to_days(period: str) -> int:
|
|
147
|
+
"""Parse period string to number of days.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
period: Period string (1d, 1w, 1m, 3m, 6m, 1y, 2y, 5y, ytd, all)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Number of calendar days
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: Invalid period format
|
|
157
|
+
"""
|
|
158
|
+
period = period.lower().strip()
|
|
159
|
+
|
|
160
|
+
if period == "ytd":
|
|
161
|
+
today = dt.date.today()
|
|
162
|
+
year_start = dt.date(today.year, 1, 1)
|
|
163
|
+
return (today - year_start).days
|
|
164
|
+
|
|
165
|
+
if period == "all" or period == "max":
|
|
166
|
+
return 365 * 10 # 10 years max
|
|
167
|
+
|
|
168
|
+
# Parse numeric periods
|
|
169
|
+
if period.endswith("d"):
|
|
170
|
+
return int(period[:-1])
|
|
171
|
+
elif period.endswith("w"):
|
|
172
|
+
return int(period[:-1]) * 7
|
|
173
|
+
elif period.endswith("m"):
|
|
174
|
+
return int(period[:-1]) * 30
|
|
175
|
+
elif period.endswith("y"):
|
|
176
|
+
return int(period[:-1]) * 365
|
|
177
|
+
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Invalid period format: {period}. Use: 1d, 1w, 1m, 3m, 6m, 1y, 2y, 5y, ytd, all"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def period_to_market_period(period: str) -> str:
|
|
184
|
+
"""Convert our period format to market provider period format.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
period: Our period format (1m, 3m, 6m, 1y, ytd, all)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Market provider period format (1mo, 3mo, 6mo, 1y, ytd, max)
|
|
191
|
+
"""
|
|
192
|
+
period = period.lower().strip()
|
|
193
|
+
|
|
194
|
+
# Map our periods to yahooquery/provider periods
|
|
195
|
+
period_map = {
|
|
196
|
+
"1d": "1d",
|
|
197
|
+
"5d": "5d",
|
|
198
|
+
"1w": "5d",
|
|
199
|
+
"1m": "1mo",
|
|
200
|
+
"3m": "3mo",
|
|
201
|
+
"6m": "6mo",
|
|
202
|
+
"1y": "1y",
|
|
203
|
+
"2y": "2y",
|
|
204
|
+
"5y": "5y",
|
|
205
|
+
"10y": "10y",
|
|
206
|
+
"ytd": "ytd",
|
|
207
|
+
"all": "max",
|
|
208
|
+
"max": "max",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return period_map.get(period, "1y")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# Core Functions
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def get_benchmark_history(
|
|
220
|
+
symbol: str,
|
|
221
|
+
*,
|
|
222
|
+
period: str = "1y",
|
|
223
|
+
market_provider: MarketDataProvider | None = None,
|
|
224
|
+
) -> BenchmarkHistory:
|
|
225
|
+
"""Fetch historical benchmark data with normalized values for charting.
|
|
226
|
+
|
|
227
|
+
This function fetches real market data from fin-infra's market data providers
|
|
228
|
+
and returns a time series normalized to 100 for easy comparison charting.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
symbol: Benchmark ticker symbol (SPY, QQQ, VTI, BND, etc.)
|
|
232
|
+
period: Time period (1d, 1w, 1m, 3m, 6m, 1y, 2y, 5y, ytd, all)
|
|
233
|
+
market_provider: Optional market data provider instance.
|
|
234
|
+
If None, creates one using easy_market().
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
BenchmarkHistory with normalized time series and summary metrics
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: Invalid symbol or period
|
|
241
|
+
Exception: Market data provider errors
|
|
242
|
+
|
|
243
|
+
Examples:
|
|
244
|
+
>>> # Using auto-configured provider
|
|
245
|
+
>>> history = await get_benchmark_history("SPY", period="1y")
|
|
246
|
+
>>> print(f"SPY 1Y return: {history.total_return_percent:.2f}%")
|
|
247
|
+
>>>
|
|
248
|
+
>>> # With custom provider
|
|
249
|
+
>>> from fin_infra.markets import easy_market
|
|
250
|
+
>>> market = easy_market(provider="yahoo")
|
|
251
|
+
>>> history = await get_benchmark_history("QQQ", period="6m", market_provider=market)
|
|
252
|
+
"""
|
|
253
|
+
# Create market provider if not provided
|
|
254
|
+
if market_provider is None:
|
|
255
|
+
from ..markets import easy_market
|
|
256
|
+
|
|
257
|
+
market_provider = easy_market()
|
|
258
|
+
|
|
259
|
+
# Validate symbol
|
|
260
|
+
symbol = symbol.upper()
|
|
261
|
+
|
|
262
|
+
# Get market period format
|
|
263
|
+
market_period = period_to_market_period(period)
|
|
264
|
+
|
|
265
|
+
logger.info(f"[Benchmark] Fetching {symbol} history for period={period} ({market_period})")
|
|
266
|
+
|
|
267
|
+
# Fetch historical candles from market provider
|
|
268
|
+
candles = market_provider.history(symbol, period=market_period, interval="1d")
|
|
269
|
+
|
|
270
|
+
if not candles:
|
|
271
|
+
raise ValueError(f"No historical data returned for {symbol}")
|
|
272
|
+
|
|
273
|
+
# Sort candles by timestamp (oldest first for normalization)
|
|
274
|
+
candles_sorted = sorted(candles, key=lambda c: c.ts)
|
|
275
|
+
|
|
276
|
+
# Get first and last prices for normalization
|
|
277
|
+
first_candle = candles_sorted[0]
|
|
278
|
+
last_candle = candles_sorted[-1]
|
|
279
|
+
first_price = float(first_candle.close)
|
|
280
|
+
last_price = float(last_candle.close)
|
|
281
|
+
|
|
282
|
+
if first_price <= 0:
|
|
283
|
+
raise ValueError(f"Invalid starting price for {symbol}: {first_price}")
|
|
284
|
+
|
|
285
|
+
# Calculate total return
|
|
286
|
+
total_return_pct = ((last_price - first_price) / first_price) * 100
|
|
287
|
+
|
|
288
|
+
# Calculate annualized return
|
|
289
|
+
days_in_period = parse_period_to_days(period)
|
|
290
|
+
if days_in_period >= 365:
|
|
291
|
+
years = days_in_period / 365
|
|
292
|
+
annualized_return = ((1 + total_return_pct / 100) ** (1 / years) - 1) * 100
|
|
293
|
+
else:
|
|
294
|
+
annualized_return = None
|
|
295
|
+
|
|
296
|
+
# Build normalized data points
|
|
297
|
+
data_points: list[BenchmarkDataPoint] = []
|
|
298
|
+
for candle in candles_sorted:
|
|
299
|
+
# Convert timestamp to date
|
|
300
|
+
candle_dt = dt.datetime.fromtimestamp(candle.ts / 1000, tz=dt.UTC)
|
|
301
|
+
close_price = float(candle.close)
|
|
302
|
+
|
|
303
|
+
# Normalize to 100
|
|
304
|
+
normalized = (close_price / first_price) * 100
|
|
305
|
+
return_pct = normalized - 100
|
|
306
|
+
|
|
307
|
+
data_points.append(
|
|
308
|
+
BenchmarkDataPoint(
|
|
309
|
+
date=candle_dt.date(),
|
|
310
|
+
close=close_price,
|
|
311
|
+
normalized=round(normalized, 2),
|
|
312
|
+
return_pct=round(return_pct, 2),
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Determine start and end dates
|
|
317
|
+
start_date = data_points[0].date if data_points else dt.date.today()
|
|
318
|
+
end_date = data_points[-1].date if data_points else dt.date.today()
|
|
319
|
+
|
|
320
|
+
logger.info(
|
|
321
|
+
f"[Benchmark] {symbol}: {len(data_points)} points, "
|
|
322
|
+
f"{start_date} to {end_date}, return={total_return_pct:.2f}%"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return BenchmarkHistory(
|
|
326
|
+
symbol=symbol,
|
|
327
|
+
period=period,
|
|
328
|
+
start_date=start_date,
|
|
329
|
+
end_date=end_date,
|
|
330
|
+
data_points=data_points,
|
|
331
|
+
start_price=first_price,
|
|
332
|
+
end_price=last_price,
|
|
333
|
+
total_return_percent=round(total_return_pct, 2),
|
|
334
|
+
annualized_return_percent=round(annualized_return, 2) if annualized_return else None,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def compare_portfolio_to_benchmark(
|
|
339
|
+
portfolio_values: Sequence[tuple[dt.date, float]],
|
|
340
|
+
*,
|
|
341
|
+
benchmark: str = "SPY",
|
|
342
|
+
period: str | None = None,
|
|
343
|
+
market_provider: MarketDataProvider | None = None,
|
|
344
|
+
risk_free_rate: float = 0.03,
|
|
345
|
+
) -> PortfolioVsBenchmark:
|
|
346
|
+
"""Compare portfolio performance to a benchmark index.
|
|
347
|
+
|
|
348
|
+
Takes portfolio historical values and compares them to benchmark performance,
|
|
349
|
+
calculating alpha, beta, and providing normalized time series for charting.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
portfolio_values: List of (date, value) tuples representing portfolio history.
|
|
353
|
+
Values should be total portfolio value on each date.
|
|
354
|
+
benchmark: Benchmark ticker symbol (default: SPY)
|
|
355
|
+
period: Time period override. If None, uses the date range from portfolio_values.
|
|
356
|
+
market_provider: Optional market data provider instance.
|
|
357
|
+
risk_free_rate: Annual risk-free rate for Sharpe calculation (default: 0.03 = 3%)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
PortfolioVsBenchmark with comparison metrics and time series
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ValueError: Invalid input or insufficient data
|
|
364
|
+
|
|
365
|
+
Examples:
|
|
366
|
+
>>> # Compare portfolio to S&P 500
|
|
367
|
+
>>> portfolio_history = [
|
|
368
|
+
... (date(2024, 1, 1), 100000),
|
|
369
|
+
... (date(2024, 2, 1), 102000),
|
|
370
|
+
... (date(2024, 3, 1), 105000),
|
|
371
|
+
... # ...
|
|
372
|
+
... ]
|
|
373
|
+
>>> comparison = await compare_portfolio_to_benchmark(
|
|
374
|
+
... portfolio_history,
|
|
375
|
+
... benchmark="SPY",
|
|
376
|
+
... )
|
|
377
|
+
>>> print(f"Alpha: {comparison.alpha:.2f}%")
|
|
378
|
+
>>> print(f"Portfolio: {comparison.portfolio_return_percent:.2f}%")
|
|
379
|
+
>>> print(f"Benchmark: {comparison.benchmark_return_percent:.2f}%")
|
|
380
|
+
"""
|
|
381
|
+
if not portfolio_values:
|
|
382
|
+
raise ValueError("portfolio_values cannot be empty")
|
|
383
|
+
|
|
384
|
+
# Sort by date
|
|
385
|
+
sorted_values = sorted(portfolio_values, key=lambda x: x[0])
|
|
386
|
+
start_date = sorted_values[0][0]
|
|
387
|
+
end_date = sorted_values[-1][0]
|
|
388
|
+
|
|
389
|
+
# Calculate portfolio return
|
|
390
|
+
first_value = sorted_values[0][1]
|
|
391
|
+
last_value = sorted_values[-1][1]
|
|
392
|
+
|
|
393
|
+
if first_value <= 0:
|
|
394
|
+
raise ValueError(f"Invalid starting portfolio value: {first_value}")
|
|
395
|
+
|
|
396
|
+
portfolio_return_pct = ((last_value - first_value) / first_value) * 100
|
|
397
|
+
|
|
398
|
+
# Calculate period from data range if not specified
|
|
399
|
+
if period is None:
|
|
400
|
+
days_diff = (end_date - start_date).days
|
|
401
|
+
if days_diff <= 30:
|
|
402
|
+
period = "1m"
|
|
403
|
+
elif days_diff <= 90:
|
|
404
|
+
period = "3m"
|
|
405
|
+
elif days_diff <= 180:
|
|
406
|
+
period = "6m"
|
|
407
|
+
elif days_diff <= 365:
|
|
408
|
+
period = "1y"
|
|
409
|
+
else:
|
|
410
|
+
period = "all"
|
|
411
|
+
|
|
412
|
+
# Fetch benchmark history
|
|
413
|
+
benchmark_history = await get_benchmark_history(
|
|
414
|
+
benchmark,
|
|
415
|
+
period=period,
|
|
416
|
+
market_provider=market_provider,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
benchmark_return_pct = benchmark_history.total_return_percent
|
|
420
|
+
|
|
421
|
+
# Calculate alpha (simple excess return)
|
|
422
|
+
alpha = portfolio_return_pct - benchmark_return_pct
|
|
423
|
+
|
|
424
|
+
# Calculate beta (requires daily returns - simplified calculation)
|
|
425
|
+
beta = _calculate_beta_simple(sorted_values, benchmark_history.data_points)
|
|
426
|
+
|
|
427
|
+
# Calculate Sharpe ratio (simplified - uses portfolio return vs risk-free)
|
|
428
|
+
# For proper Sharpe, would need daily returns and standard deviation
|
|
429
|
+
sharpe = _calculate_sharpe_simple(
|
|
430
|
+
portfolio_return_pct,
|
|
431
|
+
risk_free_rate * 100, # Convert to percentage
|
|
432
|
+
period,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Build normalized portfolio series
|
|
436
|
+
portfolio_series: list[BenchmarkDataPoint] = []
|
|
437
|
+
for value_date, value in sorted_values:
|
|
438
|
+
normalized = (value / first_value) * 100
|
|
439
|
+
return_pct = normalized - 100
|
|
440
|
+
portfolio_series.append(
|
|
441
|
+
BenchmarkDataPoint(
|
|
442
|
+
date=value_date,
|
|
443
|
+
close=value,
|
|
444
|
+
normalized=round(normalized, 2),
|
|
445
|
+
return_pct=round(return_pct, 2),
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return PortfolioVsBenchmark(
|
|
450
|
+
benchmark_symbol=benchmark,
|
|
451
|
+
period=period,
|
|
452
|
+
start_date=start_date,
|
|
453
|
+
end_date=end_date,
|
|
454
|
+
portfolio_return_percent=round(portfolio_return_pct, 2),
|
|
455
|
+
benchmark_return_percent=round(benchmark_return_pct, 2),
|
|
456
|
+
alpha=round(alpha, 2),
|
|
457
|
+
beta=round(beta, 2) if beta is not None else None,
|
|
458
|
+
sharpe_ratio=round(sharpe, 2) if sharpe is not None else None,
|
|
459
|
+
portfolio_series=portfolio_series,
|
|
460
|
+
benchmark_series=benchmark_history.data_points,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _calculate_beta_simple(
|
|
465
|
+
portfolio_values: Sequence[tuple[dt.date, float]],
|
|
466
|
+
benchmark_points: Sequence[BenchmarkDataPoint],
|
|
467
|
+
) -> float | None:
|
|
468
|
+
"""Calculate simplified beta from value series.
|
|
469
|
+
|
|
470
|
+
Uses simplified covariance/variance calculation. For proper beta,
|
|
471
|
+
would need more sophisticated time series alignment and daily returns.
|
|
472
|
+
|
|
473
|
+
Returns None if insufficient data.
|
|
474
|
+
"""
|
|
475
|
+
if len(portfolio_values) < 5 or len(benchmark_points) < 5:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
# Calculate daily returns for portfolio
|
|
479
|
+
portfolio_returns: list[float] = []
|
|
480
|
+
for i in range(1, len(portfolio_values)):
|
|
481
|
+
prev_val = portfolio_values[i - 1][1]
|
|
482
|
+
curr_val = portfolio_values[i][1]
|
|
483
|
+
if prev_val > 0:
|
|
484
|
+
ret = (curr_val - prev_val) / prev_val
|
|
485
|
+
portfolio_returns.append(ret)
|
|
486
|
+
|
|
487
|
+
# Calculate daily returns for benchmark
|
|
488
|
+
benchmark_returns: list[float] = []
|
|
489
|
+
for i in range(1, len(benchmark_points)):
|
|
490
|
+
prev_close = benchmark_points[i - 1].close
|
|
491
|
+
curr_close = benchmark_points[i].close
|
|
492
|
+
if prev_close > 0:
|
|
493
|
+
ret = (curr_close - prev_close) / prev_close
|
|
494
|
+
benchmark_returns.append(ret)
|
|
495
|
+
|
|
496
|
+
if len(portfolio_returns) < 3 or len(benchmark_returns) < 3:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# Use the shorter length for alignment
|
|
500
|
+
n = min(len(portfolio_returns), len(benchmark_returns))
|
|
501
|
+
p_returns = portfolio_returns[-n:]
|
|
502
|
+
b_returns = benchmark_returns[-n:]
|
|
503
|
+
|
|
504
|
+
# Calculate means
|
|
505
|
+
p_mean = sum(p_returns) / n
|
|
506
|
+
b_mean = sum(b_returns) / n
|
|
507
|
+
|
|
508
|
+
# Calculate covariance and variance
|
|
509
|
+
covariance = sum((p - p_mean) * (b - b_mean) for p, b in zip(p_returns, b_returns)) / n
|
|
510
|
+
variance = sum((b - b_mean) ** 2 for b in b_returns) / n
|
|
511
|
+
|
|
512
|
+
if variance == 0:
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
return covariance / variance
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _calculate_sharpe_simple(
|
|
519
|
+
portfolio_return_pct: float,
|
|
520
|
+
risk_free_rate_pct: float,
|
|
521
|
+
period: str,
|
|
522
|
+
) -> float | None:
|
|
523
|
+
"""Calculate simplified Sharpe ratio.
|
|
524
|
+
|
|
525
|
+
Uses a simplified volatility estimate based on period.
|
|
526
|
+
For proper Sharpe, would need daily return standard deviation.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
portfolio_return_pct: Portfolio return percentage
|
|
530
|
+
risk_free_rate_pct: Risk-free rate percentage (annualized)
|
|
531
|
+
period: Time period for volatility estimation
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Simplified Sharpe ratio or None if cannot calculate
|
|
535
|
+
"""
|
|
536
|
+
# Annualize the portfolio return if needed
|
|
537
|
+
days = parse_period_to_days(period)
|
|
538
|
+
|
|
539
|
+
if days < 30:
|
|
540
|
+
return None # Too short a period for meaningful Sharpe
|
|
541
|
+
|
|
542
|
+
# Annualize return
|
|
543
|
+
if days < 365:
|
|
544
|
+
annualized_return = portfolio_return_pct * (365 / days)
|
|
545
|
+
else:
|
|
546
|
+
years = days / 365
|
|
547
|
+
annualized_return = ((1 + portfolio_return_pct / 100) ** (1 / years) - 1) * 100
|
|
548
|
+
|
|
549
|
+
# Excess return
|
|
550
|
+
excess_return = annualized_return - risk_free_rate_pct
|
|
551
|
+
|
|
552
|
+
# Estimate volatility (rough estimate: 15% for diversified portfolio)
|
|
553
|
+
# This is a simplification - proper Sharpe needs actual std dev of returns
|
|
554
|
+
estimated_volatility = 15.0
|
|
555
|
+
|
|
556
|
+
if estimated_volatility == 0:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
return excess_return / estimated_volatility
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# ============================================================================
|
|
563
|
+
# Utility Functions
|
|
564
|
+
# ============================================================================
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def list_common_benchmarks() -> dict[str, str]:
|
|
568
|
+
"""Return dictionary of commonly used benchmark symbols and names.
|
|
569
|
+
|
|
570
|
+
This is a REFERENCE list only. fin-infra allows ANY valid ticker
|
|
571
|
+
to be used as a benchmark via get_benchmark_history().
|
|
572
|
+
|
|
573
|
+
Application layers should define their own allowed lists if needed.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Dict mapping symbol to full name (e.g., {"SPY": "S&P 500 (SPDR)"})
|
|
577
|
+
"""
|
|
578
|
+
return COMMON_BENCHMARKS.copy()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def is_common_benchmark(symbol: str) -> bool:
|
|
582
|
+
"""Check if a symbol is in the common benchmarks reference list.
|
|
583
|
+
|
|
584
|
+
Note: This does NOT validate whether a ticker is usable.
|
|
585
|
+
Any valid stock/ETF ticker can be used with get_benchmark_history().
|
|
586
|
+
This function only checks if it's in the commonly-used reference list.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
symbol: Ticker symbol to check
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
True if symbol is in the common benchmarks reference list
|
|
593
|
+
"""
|
|
594
|
+
return symbol.upper() in COMMON_BENCHMARKS
|