wbfdm 1.43.1__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of wbfdm might be problematic. Click here for more details.
- wbfdm/__init__.py +2 -0
- wbfdm/admin/__init__.py +42 -0
- wbfdm/admin/classifications.py +39 -0
- wbfdm/admin/esg.py +23 -0
- wbfdm/admin/exchanges.py +53 -0
- wbfdm/admin/instrument_lists.py +23 -0
- wbfdm/admin/instrument_prices.py +62 -0
- wbfdm/admin/instrument_requests.py +33 -0
- wbfdm/admin/instruments.py +117 -0
- wbfdm/admin/instruments_relationships.py +25 -0
- wbfdm/admin/options.py +101 -0
- wbfdm/analysis/__init__.py +2 -0
- wbfdm/analysis/esg/__init__.py +0 -0
- wbfdm/analysis/esg/enums.py +82 -0
- wbfdm/analysis/esg/esg_analysis.py +217 -0
- wbfdm/analysis/esg/utils.py +13 -0
- wbfdm/analysis/financial_analysis/__init__.py +1 -0
- wbfdm/analysis/financial_analysis/financial_metric_analysis.py +88 -0
- wbfdm/analysis/financial_analysis/financial_ratio_analysis.py +125 -0
- wbfdm/analysis/financial_analysis/financial_statistics_analysis.py +277 -0
- wbfdm/analysis/financial_analysis/statement_with_estimates.py +558 -0
- wbfdm/analysis/financial_analysis/utils.py +316 -0
- wbfdm/analysis/technical_analysis/__init__.py +1 -0
- wbfdm/analysis/technical_analysis/technical_analysis.py +138 -0
- wbfdm/analysis/technical_analysis/traces.py +165 -0
- wbfdm/analysis/utils.py +32 -0
- wbfdm/apps.py +14 -0
- wbfdm/backends/dto.py +36 -0
- wbfdm/contrib/__init__.py +0 -0
- wbfdm/contrib/dsws/__init__.py +0 -0
- wbfdm/contrib/dsws/client.py +285 -0
- wbfdm/contrib/dsws/dataloaders/market_data.py +130 -0
- wbfdm/contrib/internal/__init__.py +0 -0
- wbfdm/contrib/internal/dataloaders/__init__.py +0 -0
- wbfdm/contrib/internal/dataloaders/market_data.py +87 -0
- wbfdm/contrib/metric/__init__.py +0 -0
- wbfdm/contrib/metric/admin/__init__.py +2 -0
- wbfdm/contrib/metric/admin/instruments.py +12 -0
- wbfdm/contrib/metric/admin/metrics.py +43 -0
- wbfdm/contrib/metric/apps.py +10 -0
- wbfdm/contrib/metric/backends/__init__.py +2 -0
- wbfdm/contrib/metric/backends/base.py +159 -0
- wbfdm/contrib/metric/backends/performances.py +265 -0
- wbfdm/contrib/metric/backends/statistics.py +182 -0
- wbfdm/contrib/metric/decorators.py +14 -0
- wbfdm/contrib/metric/dispatch.py +23 -0
- wbfdm/contrib/metric/dto.py +88 -0
- wbfdm/contrib/metric/exceptions.py +6 -0
- wbfdm/contrib/metric/factories.py +33 -0
- wbfdm/contrib/metric/filters.py +28 -0
- wbfdm/contrib/metric/migrations/0001_initial.py +88 -0
- wbfdm/contrib/metric/migrations/0002_remove_instrumentmetric_unique_instrument_metric_and_more.py +26 -0
- wbfdm/contrib/metric/migrations/__init__.py +0 -0
- wbfdm/contrib/metric/models.py +180 -0
- wbfdm/contrib/metric/orchestrators.py +94 -0
- wbfdm/contrib/metric/registry.py +80 -0
- wbfdm/contrib/metric/serializers.py +44 -0
- wbfdm/contrib/metric/tasks.py +27 -0
- wbfdm/contrib/metric/tests/__init__.py +0 -0
- wbfdm/contrib/metric/tests/backends/__init__.py +0 -0
- wbfdm/contrib/metric/tests/backends/test_performances.py +152 -0
- wbfdm/contrib/metric/tests/backends/test_statistics.py +48 -0
- wbfdm/contrib/metric/tests/conftest.py +92 -0
- wbfdm/contrib/metric/tests/test_dto.py +73 -0
- wbfdm/contrib/metric/tests/test_models.py +72 -0
- wbfdm/contrib/metric/tests/test_tasks.py +24 -0
- wbfdm/contrib/metric/tests/test_viewsets.py +79 -0
- wbfdm/contrib/metric/urls.py +19 -0
- wbfdm/contrib/metric/viewsets/__init__.py +1 -0
- wbfdm/contrib/metric/viewsets/configs/__init__.py +1 -0
- wbfdm/contrib/metric/viewsets/configs/display.py +92 -0
- wbfdm/contrib/metric/viewsets/configs/menus.py +11 -0
- wbfdm/contrib/metric/viewsets/configs/utils.py +137 -0
- wbfdm/contrib/metric/viewsets/mixins.py +248 -0
- wbfdm/contrib/metric/viewsets/viewsets.py +40 -0
- wbfdm/contrib/msci/__init__.py +0 -0
- wbfdm/contrib/msci/client.py +92 -0
- wbfdm/contrib/msci/dataloaders/__init__.py +0 -0
- wbfdm/contrib/msci/dataloaders/esg.py +87 -0
- wbfdm/contrib/msci/dataloaders/esg_controversies.py +81 -0
- wbfdm/contrib/msci/sync.py +58 -0
- wbfdm/contrib/msci/tests/__init__.py +0 -0
- wbfdm/contrib/msci/tests/conftest.py +1 -0
- wbfdm/contrib/msci/tests/test_client.py +70 -0
- wbfdm/contrib/qa/__init__.py +0 -0
- wbfdm/contrib/qa/apps.py +22 -0
- wbfdm/contrib/qa/database_routers.py +25 -0
- wbfdm/contrib/qa/dataloaders/__init__.py +0 -0
- wbfdm/contrib/qa/dataloaders/adjustments.py +56 -0
- wbfdm/contrib/qa/dataloaders/corporate_actions.py +59 -0
- wbfdm/contrib/qa/dataloaders/financials.py +83 -0
- wbfdm/contrib/qa/dataloaders/market_data.py +117 -0
- wbfdm/contrib/qa/dataloaders/officers.py +59 -0
- wbfdm/contrib/qa/dataloaders/reporting_dates.py +67 -0
- wbfdm/contrib/qa/dataloaders/statements.py +267 -0
- wbfdm/contrib/qa/jinja2/qa/sql/companies.sql +100 -0
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/base_estimates.sql +33 -0
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/calendarized.sql +37 -0
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/complete.sql +9 -0
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/estimates.sql +3 -0
- wbfdm/contrib/qa/jinja2/qa/sql/ibes/financials.sql +79 -0
- wbfdm/contrib/qa/jinja2/qa/sql/instruments.sql +100 -0
- wbfdm/contrib/qa/jinja2/qa/sql/quotes.sql +98 -0
- wbfdm/contrib/qa/sync/exchanges.py +70 -0
- wbfdm/contrib/qa/sync/instruments.py +94 -0
- wbfdm/contrib/qa/sync/utils.py +241 -0
- wbfdm/contrib/qa/tasks.py +0 -0
- wbfdm/dataloaders/__init__.py +0 -0
- wbfdm/dataloaders/cache.py +129 -0
- wbfdm/dataloaders/protocols.py +112 -0
- wbfdm/dataloaders/proxies.py +201 -0
- wbfdm/dataloaders/types.py +209 -0
- wbfdm/dynamic_preferences_registry.py +45 -0
- wbfdm/enums.py +657 -0
- wbfdm/factories/__init__.py +13 -0
- wbfdm/factories/classifications.py +56 -0
- wbfdm/factories/controversies.py +27 -0
- wbfdm/factories/exchanges.py +21 -0
- wbfdm/factories/instrument_list.py +22 -0
- wbfdm/factories/instrument_prices.py +79 -0
- wbfdm/factories/instruments.py +63 -0
- wbfdm/factories/instruments_relationships.py +31 -0
- wbfdm/factories/options.py +66 -0
- wbfdm/figures/__init__.py +1 -0
- wbfdm/figures/financials/__init__.py +1 -0
- wbfdm/figures/financials/financial_analysis_charts.py +469 -0
- wbfdm/figures/financials/financials_charts.py +711 -0
- wbfdm/filters/__init__.py +31 -0
- wbfdm/filters/classifications.py +100 -0
- wbfdm/filters/exchanges.py +22 -0
- wbfdm/filters/financials.py +95 -0
- wbfdm/filters/financials_analysis.py +119 -0
- wbfdm/filters/instrument_prices.py +112 -0
- wbfdm/filters/instruments.py +198 -0
- wbfdm/filters/utils.py +44 -0
- wbfdm/import_export/__init__.py +0 -0
- wbfdm/import_export/backends/__init__.py +0 -0
- wbfdm/import_export/backends/cbinsights/__init__.py +2 -0
- wbfdm/import_export/backends/cbinsights/deals.py +44 -0
- wbfdm/import_export/backends/cbinsights/equities.py +41 -0
- wbfdm/import_export/backends/cbinsights/mixin.py +15 -0
- wbfdm/import_export/backends/cbinsights/utils/__init__.py +0 -0
- wbfdm/import_export/backends/cbinsights/utils/classifications.py +4150 -0
- wbfdm/import_export/backends/cbinsights/utils/client.py +217 -0
- wbfdm/import_export/backends/refinitiv/__init__.py +5 -0
- wbfdm/import_export/backends/refinitiv/daily_fundamental.py +36 -0
- wbfdm/import_export/backends/refinitiv/fiscal_period.py +63 -0
- wbfdm/import_export/backends/refinitiv/forecast.py +178 -0
- wbfdm/import_export/backends/refinitiv/fundamental.py +103 -0
- wbfdm/import_export/backends/refinitiv/geographic_segment.py +32 -0
- wbfdm/import_export/backends/refinitiv/instrument.py +55 -0
- wbfdm/import_export/backends/refinitiv/instrument_price.py +77 -0
- wbfdm/import_export/backends/refinitiv/mixin.py +29 -0
- wbfdm/import_export/backends/refinitiv/utils/__init__.py +1 -0
- wbfdm/import_export/backends/refinitiv/utils/controller.py +182 -0
- wbfdm/import_export/handlers/__init__.py +0 -0
- wbfdm/import_export/handlers/instrument.py +253 -0
- wbfdm/import_export/handlers/instrument_list.py +101 -0
- wbfdm/import_export/handlers/instrument_price.py +71 -0
- wbfdm/import_export/handlers/option.py +54 -0
- wbfdm/import_export/handlers/private_equities.py +49 -0
- wbfdm/import_export/parsers/__init__.py +0 -0
- wbfdm/import_export/parsers/cbinsights/__init__.py +0 -0
- wbfdm/import_export/parsers/cbinsights/deals.py +39 -0
- wbfdm/import_export/parsers/cbinsights/equities.py +56 -0
- wbfdm/import_export/parsers/cbinsights/fundamentals.py +45 -0
- wbfdm/import_export/parsers/refinitiv/__init__.py +0 -0
- wbfdm/import_export/parsers/refinitiv/daily_fundamental.py +7 -0
- wbfdm/import_export/parsers/refinitiv/forecast.py +7 -0
- wbfdm/import_export/parsers/refinitiv/fundamental.py +9 -0
- wbfdm/import_export/parsers/refinitiv/geographic_segment.py +7 -0
- wbfdm/import_export/parsers/refinitiv/instrument.py +75 -0
- wbfdm/import_export/parsers/refinitiv/instrument_price.py +26 -0
- wbfdm/import_export/parsers/refinitiv/utils.py +96 -0
- wbfdm/import_export/resources/__init__.py +0 -0
- wbfdm/import_export/resources/classification.py +23 -0
- wbfdm/import_export/resources/instrument_prices.py +33 -0
- wbfdm/import_export/resources/instruments.py +176 -0
- wbfdm/jinja2.py +7 -0
- wbfdm/management/__init__.py +30 -0
- wbfdm/menu.py +11 -0
- wbfdm/migrations/0001_initial.py +71 -0
- wbfdm/migrations/0002_rename_statements_instrumentlookup_financials_and_more.py +144 -0
- wbfdm/migrations/0003_instrument_estimate_backend_and_more.py +34 -0
- wbfdm/migrations/0004_rename_financials_instrumentlookup_statements_and_more.py +86 -0
- wbfdm/migrations/0005_instrument_corporate_action_backend.py +29 -0
- wbfdm/migrations/0006_instrument_officer_backend.py +29 -0
- wbfdm/migrations/0007_instrument_country_instrument_currency_and_more.py +117 -0
- wbfdm/migrations/0008_controversy.py +75 -0
- wbfdm/migrations/0009_alter_controversy_flag_alter_controversy_initiated_and_more.py +85 -0
- wbfdm/migrations/0010_classification_classificationgroup_deal_exchange_and_more.py +1299 -0
- wbfdm/migrations/0011_delete_instrumentlookup_instrument_corporate_actions_and_more.py +169 -0
- wbfdm/migrations/0012_instrumentprice_created_instrumentprice_modified.py +564 -0
- wbfdm/migrations/0013_instrument_is_investable_universe_and_more.py +199 -0
- wbfdm/migrations/0014_alter_controversy_instrument.py +22 -0
- wbfdm/migrations/0015_instrument_instrument_investible_index.py +16 -0
- wbfdm/migrations/0016_instrumenttype_name_repr.py +18 -0
- wbfdm/migrations/0017_instrument_instrument_security_index.py +16 -0
- wbfdm/migrations/0018_instrument_instrument_level_index.py +20 -0
- wbfdm/migrations/0019_alter_controversy_source.py +17 -0
- wbfdm/migrations/0020_optionaggregate_option_and_more.py +249 -0
- wbfdm/migrations/0021_delete_instrumentdailystatistics.py +15 -0
- wbfdm/migrations/0022_instrument_cusip_option_open_interest_20d_and_more.py +91 -0
- wbfdm/migrations/0023_instrument_unique_ric_instrument_unique_rmc_and_more.py +53 -0
- wbfdm/migrations/0024_option_open_interest_10d_option_volume_10d_and_more.py +36 -0
- wbfdm/migrations/0025_instrument_is_primary_and_more.py +29 -0
- wbfdm/migrations/0026_instrument_is_cash_equivalent.py +30 -0
- wbfdm/migrations/0027_remove_instrument_unique_ric_and_more.py +100 -0
- wbfdm/migrations/0028_instrumentprice_annualized_daily_volatility.py +17 -0
- wbfdm/migrations/__init__.py +0 -0
- wbfdm/models/__init__.py +4 -0
- wbfdm/models/esg/__init__.py +1 -0
- wbfdm/models/esg/controversies.py +81 -0
- wbfdm/models/exchanges/__init__.py +1 -0
- wbfdm/models/exchanges/exchanges.py +223 -0
- wbfdm/models/fields.py +117 -0
- wbfdm/models/fk_fields.py +403 -0
- wbfdm/models/indicators.py +0 -0
- wbfdm/models/instruments/__init__.py +19 -0
- wbfdm/models/instruments/classifications.py +265 -0
- wbfdm/models/instruments/instrument_lists.py +120 -0
- wbfdm/models/instruments/instrument_prices.py +544 -0
- wbfdm/models/instruments/instrument_relationships.py +251 -0
- wbfdm/models/instruments/instrument_requests.py +196 -0
- wbfdm/models/instruments/instruments.py +991 -0
- wbfdm/models/instruments/llm/__init__.py +1 -0
- wbfdm/models/instruments/llm/create_instrument_news_relationships.py +78 -0
- wbfdm/models/instruments/mixin/__init__.py +0 -0
- wbfdm/models/instruments/mixin/financials_computed.py +804 -0
- wbfdm/models/instruments/mixin/financials_serializer_fields.py +1407 -0
- wbfdm/models/instruments/mixin/instruments.py +297 -0
- wbfdm/models/instruments/options.py +225 -0
- wbfdm/models/instruments/private_equities.py +59 -0
- wbfdm/models/instruments/querysets.py +73 -0
- wbfdm/models/instruments/utils.py +41 -0
- wbfdm/preferences.py +21 -0
- wbfdm/serializers/__init__.py +4 -0
- wbfdm/serializers/esg.py +36 -0
- wbfdm/serializers/exchanges.py +39 -0
- wbfdm/serializers/instruments/__init__.py +37 -0
- wbfdm/serializers/instruments/classifications.py +139 -0
- wbfdm/serializers/instruments/instrument_lists.py +61 -0
- wbfdm/serializers/instruments/instrument_prices.py +73 -0
- wbfdm/serializers/instruments/instrument_relationships.py +170 -0
- wbfdm/serializers/instruments/instrument_requests.py +61 -0
- wbfdm/serializers/instruments/instruments.py +274 -0
- wbfdm/serializers/instruments/mixins.py +104 -0
- wbfdm/serializers/officers.py +20 -0
- wbfdm/signals.py +7 -0
- wbfdm/sync/__init__.py +0 -0
- wbfdm/sync/abstract.py +31 -0
- wbfdm/sync/runner.py +22 -0
- wbfdm/tasks.py +69 -0
- wbfdm/tests/__init__.py +0 -0
- wbfdm/tests/analysis/__init__.py +0 -0
- wbfdm/tests/analysis/financial_analysis/__init__.py +0 -0
- wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +392 -0
- wbfdm/tests/analysis/financial_analysis/test_utils.py +322 -0
- wbfdm/tests/analysis/test_esg.py +159 -0
- wbfdm/tests/conftest.py +92 -0
- wbfdm/tests/dataloaders/__init__.py +0 -0
- wbfdm/tests/dataloaders/test_cache.py +73 -0
- wbfdm/tests/models/__init__.py +0 -0
- wbfdm/tests/models/test_classifications.py +99 -0
- wbfdm/tests/models/test_exchanges.py +7 -0
- wbfdm/tests/models/test_instrument_list.py +117 -0
- wbfdm/tests/models/test_instrument_prices.py +306 -0
- wbfdm/tests/models/test_instruments.py +202 -0
- wbfdm/tests/models/test_merge.py +99 -0
- wbfdm/tests/models/test_options.py +69 -0
- wbfdm/tests/test_tasks.py +6 -0
- wbfdm/tests/tests.py +10 -0
- wbfdm/urls.py +222 -0
- wbfdm/utils.py +54 -0
- wbfdm/viewsets/__init__.py +10 -0
- wbfdm/viewsets/configs/__init__.py +5 -0
- wbfdm/viewsets/configs/buttons/__init__.py +8 -0
- wbfdm/viewsets/configs/buttons/classifications.py +23 -0
- wbfdm/viewsets/configs/buttons/exchanges.py +9 -0
- wbfdm/viewsets/configs/buttons/instrument_prices.py +49 -0
- wbfdm/viewsets/configs/buttons/instruments.py +283 -0
- wbfdm/viewsets/configs/display/__init__.py +22 -0
- wbfdm/viewsets/configs/display/classifications.py +138 -0
- wbfdm/viewsets/configs/display/esg.py +75 -0
- wbfdm/viewsets/configs/display/exchanges.py +42 -0
- wbfdm/viewsets/configs/display/instrument_lists.py +137 -0
- wbfdm/viewsets/configs/display/instrument_prices.py +199 -0
- wbfdm/viewsets/configs/display/instrument_requests.py +116 -0
- wbfdm/viewsets/configs/display/instruments.py +340 -0
- wbfdm/viewsets/configs/display/instruments_relationships.py +65 -0
- wbfdm/viewsets/configs/display/monthly_performances.py +72 -0
- wbfdm/viewsets/configs/display/officers.py +16 -0
- wbfdm/viewsets/configs/display/prices.py +21 -0
- wbfdm/viewsets/configs/display/statement_with_estimates.py +101 -0
- wbfdm/viewsets/configs/display/statements.py +48 -0
- wbfdm/viewsets/configs/endpoints/__init__.py +41 -0
- wbfdm/viewsets/configs/endpoints/classifications.py +87 -0
- wbfdm/viewsets/configs/endpoints/esg.py +20 -0
- wbfdm/viewsets/configs/endpoints/exchanges.py +6 -0
- wbfdm/viewsets/configs/endpoints/financials_analysis.py +65 -0
- wbfdm/viewsets/configs/endpoints/instrument_lists.py +38 -0
- wbfdm/viewsets/configs/endpoints/instrument_prices.py +51 -0
- wbfdm/viewsets/configs/endpoints/instrument_requests.py +20 -0
- wbfdm/viewsets/configs/endpoints/instruments.py +13 -0
- wbfdm/viewsets/configs/endpoints/instruments_relationships.py +31 -0
- wbfdm/viewsets/configs/endpoints/statements.py +6 -0
- wbfdm/viewsets/configs/menus/__init__.py +9 -0
- wbfdm/viewsets/configs/menus/classifications.py +19 -0
- wbfdm/viewsets/configs/menus/exchanges.py +10 -0
- wbfdm/viewsets/configs/menus/instrument_lists.py +10 -0
- wbfdm/viewsets/configs/menus/instruments.py +20 -0
- wbfdm/viewsets/configs/menus/instruments_relationships.py +33 -0
- wbfdm/viewsets/configs/titles/__init__.py +42 -0
- wbfdm/viewsets/configs/titles/classifications.py +79 -0
- wbfdm/viewsets/configs/titles/esg.py +11 -0
- wbfdm/viewsets/configs/titles/exchanges.py +12 -0
- wbfdm/viewsets/configs/titles/financial_ratio_analysis.py +6 -0
- wbfdm/viewsets/configs/titles/financials_analysis.py +50 -0
- wbfdm/viewsets/configs/titles/instrument_prices.py +50 -0
- wbfdm/viewsets/configs/titles/instrument_requests.py +16 -0
- wbfdm/viewsets/configs/titles/instruments.py +31 -0
- wbfdm/viewsets/configs/titles/instruments_relationships.py +21 -0
- wbfdm/viewsets/configs/titles/market_data.py +13 -0
- wbfdm/viewsets/configs/titles/prices.py +15 -0
- wbfdm/viewsets/configs/titles/statement_with_estimates.py +10 -0
- wbfdm/viewsets/esg.py +72 -0
- wbfdm/viewsets/exchanges.py +63 -0
- wbfdm/viewsets/financial_analysis/__init__.py +3 -0
- wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +85 -0
- wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +85 -0
- wbfdm/viewsets/financial_analysis/statement_with_estimates.py +145 -0
- wbfdm/viewsets/instruments/__init__.py +80 -0
- wbfdm/viewsets/instruments/classifications.py +279 -0
- wbfdm/viewsets/instruments/financials_analysis.py +614 -0
- wbfdm/viewsets/instruments/instrument_lists.py +77 -0
- wbfdm/viewsets/instruments/instrument_prices.py +542 -0
- wbfdm/viewsets/instruments/instrument_requests.py +51 -0
- wbfdm/viewsets/instruments/instruments.py +106 -0
- wbfdm/viewsets/instruments/instruments_relationships.py +235 -0
- wbfdm/viewsets/instruments/utils.py +27 -0
- wbfdm/viewsets/market_data.py +172 -0
- wbfdm/viewsets/mixins.py +9 -0
- wbfdm/viewsets/officers.py +27 -0
- wbfdm/viewsets/prices.py +62 -0
- wbfdm/viewsets/statements/__init__.py +1 -0
- wbfdm/viewsets/statements/statements.py +100 -0
- wbfdm/viewsets/technical_analysis/__init__.py +1 -0
- wbfdm/viewsets/technical_analysis/monthly_performances.py +93 -0
- wbfdm-1.43.1.dist-info/METADATA +15 -0
- wbfdm-1.43.1.dist-info/RECORD +351 -0
- wbfdm-1.43.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Generator, Generic, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.db.models import F, Model, QuerySet, Value
|
|
9
|
+
from rest_framework.serializers import Field
|
|
10
|
+
from wbcore import serializers as wb_serializers
|
|
11
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
12
|
+
from wbfdm.models import Instrument, InstrumentPrice
|
|
13
|
+
|
|
14
|
+
from ..dto import Metric, MetricField, MetricKey
|
|
15
|
+
from ..exceptions import MetricInvalidParameterException
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T", bound=Model)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbstractBackend(Generic[T]):
|
|
21
|
+
BASKET_MODEL_CLASS: Type[Model]
|
|
22
|
+
keys: list[MetricKey]
|
|
23
|
+
|
|
24
|
+
def __init__(self, val_date: date | None):
|
|
25
|
+
self.val_date = val_date
|
|
26
|
+
if not self.BASKET_MODEL_CLASS:
|
|
27
|
+
raise ValueError("A class implementing AbstractBackend needs to define a BASKET_MODEL_CLASS")
|
|
28
|
+
self.content_type = ContentType.objects.get_for_model(self.BASKET_MODEL_CLASS)
|
|
29
|
+
|
|
30
|
+
def compute_metrics(self, basket: Any) -> Generator[Metric, None, None]:
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
def get_queryset(self) -> QuerySet:
|
|
34
|
+
return self.BASKET_MODEL_CLASS.objects.all()
|
|
35
|
+
|
|
36
|
+
def get_serializer_field_attr(self, metric_field: MetricField) -> dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Returns all the serializer attributes for that metric
|
|
39
|
+
|
|
40
|
+
We expect the implementing backends to override this method to define custom logics
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
"decorators": metric_field.decorators,
|
|
44
|
+
"help_text": metric_field.help_text,
|
|
45
|
+
**metric_field.serializer_kwargs,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def get_serializer_fields(
|
|
49
|
+
self, with_prefixed_key: bool = False, metric_key: MetricKey | None = None
|
|
50
|
+
) -> dict[str, Field]:
|
|
51
|
+
if metric_key is None:
|
|
52
|
+
metric_keys = self.keys
|
|
53
|
+
else:
|
|
54
|
+
metric_keys = [metric_key]
|
|
55
|
+
fields = {}
|
|
56
|
+
for metric_key in metric_keys:
|
|
57
|
+
fields.update(
|
|
58
|
+
{
|
|
59
|
+
field_key: wb_serializers.FloatField(
|
|
60
|
+
label=field_title,
|
|
61
|
+
read_only=True,
|
|
62
|
+
**self.get_serializer_field_attr(metric_key.subfields_map[field_key]),
|
|
63
|
+
)
|
|
64
|
+
for field_key, field_title in metric_key.get_fields(with_prefixed_key=with_prefixed_key)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
for extra_subfield in metric_key.extra_subfields:
|
|
68
|
+
fields[
|
|
69
|
+
f"{metric_key.key}_{extra_subfield.key}"
|
|
70
|
+
] = wb_serializers.ModelSerializer.serializer_field_mapping[extra_subfield.field_type](
|
|
71
|
+
read_only=True, label=extra_subfield.label, **self.get_serializer_field_attr(extra_subfield)
|
|
72
|
+
)
|
|
73
|
+
return fields
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InstrumentMetricBaseBackend(AbstractBackend[Instrument]):
|
|
77
|
+
BASKET_MODEL_CLASS = Instrument
|
|
78
|
+
TARGET_CURRENCY_KEY: str | None = None
|
|
79
|
+
|
|
80
|
+
def get_queryset(self) -> QuerySet[Instrument]:
|
|
81
|
+
return super().get_queryset().filter(is_investable_universe=True)
|
|
82
|
+
|
|
83
|
+
def _get_valid_date(self, instrument: Instrument) -> date:
|
|
84
|
+
if self.val_date is None and instrument.last_price_date:
|
|
85
|
+
return instrument.last_price_date
|
|
86
|
+
elif self.val_date:
|
|
87
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
88
|
+
return instrument.prices.filter(date__lte=self.val_date).latest("date").date
|
|
89
|
+
else:
|
|
90
|
+
raise MetricInvalidParameterException()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseDataloader:
|
|
94
|
+
METRIC_KEY: str
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
basket,
|
|
99
|
+
val_date: date | None = None,
|
|
100
|
+
min_date: date | None = None,
|
|
101
|
+
target_currency_key: str | None = None,
|
|
102
|
+
use_cached_metrics_key: str | None = None,
|
|
103
|
+
basket_objects: QuerySet | None = None,
|
|
104
|
+
):
|
|
105
|
+
self.val_date = val_date
|
|
106
|
+
self.min_date = min_date
|
|
107
|
+
self.basket = basket
|
|
108
|
+
self.use_cached_metrics_key = use_cached_metrics_key
|
|
109
|
+
self.target_currency_key = target_currency_key
|
|
110
|
+
self.basket_objects = basket_objects
|
|
111
|
+
if self.basket_objects is None:
|
|
112
|
+
self.basket_objects = self._get_basket_basket_objects()
|
|
113
|
+
self.basket_object_ids = list(map(lambda x: x.id, self.basket_objects))
|
|
114
|
+
self.aggregate_callback = "mean"
|
|
115
|
+
if self.target_currency_key == "USD":
|
|
116
|
+
self.fx_rate_expression = F("currency_fx_rate_to_usd__value")
|
|
117
|
+
elif self.target_currency_key:
|
|
118
|
+
self.fx_rate_expression = CurrencyFXRates.get_fx_rates_subquery_for_two_currencies(
|
|
119
|
+
"date", start_currency="instrument__currency", target_currency=self.target_currency_key
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
self.fx_rate_expression = Value(Decimal(1.0))
|
|
123
|
+
|
|
124
|
+
def _get_basket_basket_objects(self) -> QuerySet:
|
|
125
|
+
"""
|
|
126
|
+
Return an iterable of instrument contains within the basket. Expected to be override for custom logic
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
An iterable of instrument. Default to the basket itself
|
|
130
|
+
"""
|
|
131
|
+
return Instrument.objects.filter(id=self.basket.id)
|
|
132
|
+
|
|
133
|
+
def _compute(self):
|
|
134
|
+
raise NotImplementedError()
|
|
135
|
+
|
|
136
|
+
def compute(self):
|
|
137
|
+
if self.basket_object_ids:
|
|
138
|
+
if self.use_cached_metrics_key:
|
|
139
|
+
from wbfdm.contrib.metric.models import InstrumentMetric
|
|
140
|
+
|
|
141
|
+
df = pd.json_normalize(
|
|
142
|
+
InstrumentMetric.objects.filter(
|
|
143
|
+
basket_content_type=ContentType.objects.get_for_model(self.basket_objects.model),
|
|
144
|
+
basket_id__in=self.basket_object_ids,
|
|
145
|
+
key=self.use_cached_metrics_key,
|
|
146
|
+
date__isnull=True,
|
|
147
|
+
).values_list("metrics", flat=True)
|
|
148
|
+
)
|
|
149
|
+
if not df.empty:
|
|
150
|
+
if (
|
|
151
|
+
"date" in df.columns
|
|
152
|
+
): # it can happen that the cached metric don't contain a date key value, so we ffill it in case
|
|
153
|
+
df["date"] = df["date"].ffill()
|
|
154
|
+
if isinstance(self.aggregate_callback, dict):
|
|
155
|
+
df = df.agg({k: v for k, v in self.aggregate_callback.items() if k in df.columns})
|
|
156
|
+
else:
|
|
157
|
+
df = df.agg(self.aggregate_callback)
|
|
158
|
+
return df.dropna().to_dict()
|
|
159
|
+
return self._compute()
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
from typing import Any, Generator, Iterable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from dateutil.relativedelta import relativedelta
|
|
8
|
+
from django.db.models import Avg, BooleanField, DateField, F, QuerySet
|
|
9
|
+
from wbfdm.models import Instrument, InstrumentPrice, RelatedInstrumentThroughModel
|
|
10
|
+
|
|
11
|
+
from ..decorators import register
|
|
12
|
+
from ..dto import Metric, MetricField, MetricKey
|
|
13
|
+
from .base import BaseDataloader, InstrumentMetricBaseBackend
|
|
14
|
+
|
|
15
|
+
PERFORMANCE_METRIC = MetricKey(
|
|
16
|
+
key="performance",
|
|
17
|
+
label="Performance",
|
|
18
|
+
subfields=[
|
|
19
|
+
MetricField(key="daily", label="Daily", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg),
|
|
20
|
+
MetricField(key="weekly", label="Weekly", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg),
|
|
21
|
+
MetricField(
|
|
22
|
+
key="monthly", label="Monthly", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
23
|
+
),
|
|
24
|
+
MetricField(
|
|
25
|
+
key="quarterly", label="Quarterly", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
26
|
+
),
|
|
27
|
+
MetricField(key="yearly", label="Yearly", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg),
|
|
28
|
+
MetricField(
|
|
29
|
+
key="week_to_date", label="WTD", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
30
|
+
),
|
|
31
|
+
MetricField(
|
|
32
|
+
key="month_to_date", label="MTD", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
33
|
+
),
|
|
34
|
+
MetricField(
|
|
35
|
+
key="quarter_to_date", label="QTD", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
36
|
+
),
|
|
37
|
+
MetricField(
|
|
38
|
+
key="year_to_date", label="YTD", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
39
|
+
),
|
|
40
|
+
MetricField(
|
|
41
|
+
key="previous_week_to_date",
|
|
42
|
+
label="Previous Week",
|
|
43
|
+
serializer_kwargs={"percent": True, "precision": 4},
|
|
44
|
+
aggregate=Avg,
|
|
45
|
+
),
|
|
46
|
+
MetricField(
|
|
47
|
+
key="previous_month_to_date",
|
|
48
|
+
label="Previous Month",
|
|
49
|
+
serializer_kwargs={"percent": True, "precision": 4},
|
|
50
|
+
aggregate=Avg,
|
|
51
|
+
),
|
|
52
|
+
MetricField(
|
|
53
|
+
key="previous_quarter_to_date",
|
|
54
|
+
label="Previous Quarter",
|
|
55
|
+
serializer_kwargs={"percent": True, "precision": 4},
|
|
56
|
+
aggregate=Avg,
|
|
57
|
+
),
|
|
58
|
+
MetricField(
|
|
59
|
+
key="previous_year_to_date",
|
|
60
|
+
label="Previous Year",
|
|
61
|
+
serializer_kwargs={"percent": True, "precision": 4},
|
|
62
|
+
aggregate=Avg,
|
|
63
|
+
),
|
|
64
|
+
MetricField(
|
|
65
|
+
key="inception", label="Inception", serializer_kwargs={"percent": True, "precision": 4}, aggregate=Avg
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
extra_subfields=[
|
|
69
|
+
MetricField(
|
|
70
|
+
key="is_estimated",
|
|
71
|
+
label="Estimated",
|
|
72
|
+
help_text="True if the performance used a estimated price",
|
|
73
|
+
field_type=BooleanField,
|
|
74
|
+
aggregate=None,
|
|
75
|
+
),
|
|
76
|
+
MetricField(
|
|
77
|
+
key="date",
|
|
78
|
+
label="Performance Date",
|
|
79
|
+
help_text="The date at which the performances were computed",
|
|
80
|
+
field_type=DateField,
|
|
81
|
+
aggregate=None,
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
additional_prefixes=["benchmark", "peer"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
PERFORMANCE_METRIC_USD = MetricKey(
|
|
88
|
+
key="performance_usd",
|
|
89
|
+
label=PERFORMANCE_METRIC.label,
|
|
90
|
+
subfields=PERFORMANCE_METRIC.subfields,
|
|
91
|
+
additional_prefixes=PERFORMANCE_METRIC.additional_prefixes,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Dataloader(BaseDataloader):
|
|
96
|
+
METRIC_KEY = "performance"
|
|
97
|
+
|
|
98
|
+
PERFORMANCE_MAP = {
|
|
99
|
+
"weekly": 7,
|
|
100
|
+
"monthly": 30,
|
|
101
|
+
"quarterly": 120,
|
|
102
|
+
"yearly": 365,
|
|
103
|
+
"daily": "B",
|
|
104
|
+
"week_to_date": "W-FRI",
|
|
105
|
+
"month_to_date": "BME",
|
|
106
|
+
"quarter_to_date": "BQE",
|
|
107
|
+
"year_to_date": "BYE",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def __init__(self, *args, **kwargs):
|
|
111
|
+
super().__init__(*args, **kwargs)
|
|
112
|
+
self.aggregate_callback = (
|
|
113
|
+
lambda df: df.min() if df.name == "date" else df.mean()
|
|
114
|
+
) # we need to not sum the "date" column otherwise pandas crashes
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_performance_date_map(cls, pivot_date) -> dict[str, date]:
|
|
118
|
+
pivot_date = pivot_date + timedelta(days=1) - pd.tseries.offsets.BDay(1)
|
|
119
|
+
return {
|
|
120
|
+
"weekly": (pivot_date - relativedelta(weeks=1) + timedelta(days=1) - pd.tseries.offsets.BDay(1)).date(),
|
|
121
|
+
"monthly": (pivot_date - relativedelta(months=1) + timedelta(days=1) - pd.tseries.offsets.BDay(1)).date(),
|
|
122
|
+
"quarterly": (
|
|
123
|
+
pivot_date - relativedelta(months=3) + timedelta(days=1) - pd.tseries.offsets.BDay(1)
|
|
124
|
+
).date(),
|
|
125
|
+
"yearly": (pivot_date - relativedelta(years=1) + timedelta(days=1) - pd.tseries.offsets.BDay(1)).date(),
|
|
126
|
+
"daily": (pivot_date - pd.tseries.offsets.BDay(1)).date(),
|
|
127
|
+
"week_to_date": (pivot_date - pd.tseries.offsets.Week(1, weekday=4)).date(),
|
|
128
|
+
"month_to_date": (pivot_date - pd.tseries.offsets.BMonthEnd(1)).date(),
|
|
129
|
+
"quarter_to_date": (pivot_date - pd.tseries.offsets.BQuarterEnd(1)).date(),
|
|
130
|
+
"year_to_date": (pivot_date - pd.tseries.offsets.BYearEnd(1)).date(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def get_data(self) -> Iterable[tuple[Instrument, pd.Series, pd.Series]]:
|
|
134
|
+
"""
|
|
135
|
+
Helper method to return the instrument prices as a pandas Series
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
instrument: The instrument to get the prices from
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
a tuple of the prices as Series and the calculated mask (as series as well)
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
qs = (
|
|
145
|
+
InstrumentPrice.objects.filter(instrument__in=self.basket_objects, date__lte=self.val_date)
|
|
146
|
+
.annotate_base_data()
|
|
147
|
+
.annotate(fx_rate=self.fx_rate_expression, price=F("net_value") / F("fx_rate"))
|
|
148
|
+
)
|
|
149
|
+
fields = ["instrument", "date", "price", "fx_rate", "calculated"]
|
|
150
|
+
instruments_map = {i.id: i for i in self.basket_objects}
|
|
151
|
+
|
|
152
|
+
if self.min_date:
|
|
153
|
+
qs = qs.filter(date__gte=self.min_date)
|
|
154
|
+
else:
|
|
155
|
+
qs = qs.filter(date__gte=F("instrument__inception_date"))
|
|
156
|
+
recs = qs.values_list(*fields)
|
|
157
|
+
df = (
|
|
158
|
+
pd.DataFrame.from_records(recs, columns=fields)
|
|
159
|
+
.sort_values(by="calculated")
|
|
160
|
+
.groupby(["instrument", "date"])
|
|
161
|
+
.agg("first")
|
|
162
|
+
.sort_index()
|
|
163
|
+
)
|
|
164
|
+
for instrument_id, dff in df.groupby(level=0):
|
|
165
|
+
dff = dff.droplevel(0)
|
|
166
|
+
dff = dff.reindex(pd.date_range(dff.index.min(), dff.index.max()), method="ffill")
|
|
167
|
+
dff.index = pd.to_datetime(dff.index).date
|
|
168
|
+
yield instruments_map[instrument_id], dff["price"].astype(float), dff["calculated"]
|
|
169
|
+
|
|
170
|
+
def _compute(self) -> dict[str, float]:
|
|
171
|
+
"""
|
|
172
|
+
Compute the performance metrics for all the PERFORMANCE_MAP keys. If the basket is constituted of multiple instrument, take the average of each performance
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The metrics as dictionary
|
|
176
|
+
"""
|
|
177
|
+
res = {}
|
|
178
|
+
if self.val_date:
|
|
179
|
+
agg_metrics = []
|
|
180
|
+
is_estimated = False
|
|
181
|
+
for _, prices_df, calculated_df in self.get_data():
|
|
182
|
+
if not prices_df.empty and not calculated_df.empty:
|
|
183
|
+
metrics = {}
|
|
184
|
+
is_estimated = is_estimated or bool(calculated_df.iloc[-1])
|
|
185
|
+
for performance, start_date in self.get_performance_date_map(self.val_date).items():
|
|
186
|
+
with suppress(KeyError):
|
|
187
|
+
if start_price := prices_df.loc[start_date]:
|
|
188
|
+
metrics[performance] = round(prices_df.loc[self.val_date] / start_price - 1, 6)
|
|
189
|
+
previous_start_date = self.get_performance_date_map(start_date)[performance]
|
|
190
|
+
if previous_start_price := prices_df.loc[previous_start_date]:
|
|
191
|
+
metrics[f"previous_{performance}"] = round(
|
|
192
|
+
prices_df.loc[start_date] / previous_start_price - 1, 6
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not prices_df.empty and prices_df.iloc[0]:
|
|
196
|
+
metrics["inception"] = round(float(prices_df.iloc[-1] / prices_df.iloc[0] - 1), 6)
|
|
197
|
+
agg_metrics.append(metrics)
|
|
198
|
+
res = (
|
|
199
|
+
pd.DataFrame(agg_metrics).astype(float).mean(axis=0).replace([np.inf, -np.inf, np.nan], None).to_dict()
|
|
200
|
+
)
|
|
201
|
+
res["is_estimated"] = is_estimated
|
|
202
|
+
return res
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@register(move_first=True)
|
|
206
|
+
class InstrumentPerformanceMetricBackend(InstrumentMetricBaseBackend):
|
|
207
|
+
performance = PERFORMANCE_METRIC
|
|
208
|
+
keys = [PERFORMANCE_METRIC]
|
|
209
|
+
|
|
210
|
+
def get_related_instrument_relationships(self, basket) -> QuerySet[RelatedInstrumentThroughModel]:
|
|
211
|
+
if issubclass(basket.__class__, Instrument):
|
|
212
|
+
return RelatedInstrumentThroughModel.objects.filter(instrument=basket)
|
|
213
|
+
return RelatedInstrumentThroughModel.objects.none()
|
|
214
|
+
|
|
215
|
+
def compute_metrics(self, basket: Instrument) -> Generator[Metric, None, None]:
|
|
216
|
+
val_date = self._get_valid_date(basket)
|
|
217
|
+
metrics = Dataloader(basket, val_date, target_currency_key=self.TARGET_CURRENCY_KEY).compute()
|
|
218
|
+
instrument_relationships = self.get_related_instrument_relationships(basket)
|
|
219
|
+
if instrument_relationships.exists():
|
|
220
|
+
for related_type in [
|
|
221
|
+
RelatedInstrumentThroughModel.RelatedTypeChoices.BENCHMARK,
|
|
222
|
+
RelatedInstrumentThroughModel.RelatedTypeChoices.PEER,
|
|
223
|
+
]:
|
|
224
|
+
type_metrics = []
|
|
225
|
+
for rel in instrument_relationships.filter(related_type=related_type):
|
|
226
|
+
type_metrics.append(
|
|
227
|
+
Dataloader(
|
|
228
|
+
rel.related_instrument, val_date, target_currency_key=self.TARGET_CURRENCY_KEY
|
|
229
|
+
).compute()
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
type_metrics = pd.DataFrame(type_metrics).mean(axis=0).round(6)
|
|
233
|
+
for subfield in self.performance.subfields:
|
|
234
|
+
if (base_value := metrics.get(subfield.key)) and (type_value := type_metrics.get(subfield.key)):
|
|
235
|
+
metrics[f"{related_type.value.lower()}_{subfield.key}"] = round(base_value - type_value, 6)
|
|
236
|
+
metrics["date"] = val_date
|
|
237
|
+
yield Metric(
|
|
238
|
+
metrics=metrics,
|
|
239
|
+
basket_id=basket.id,
|
|
240
|
+
basket_content_type_id=self.content_type.id,
|
|
241
|
+
key=self.performance.key,
|
|
242
|
+
date=None,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def get_serializer_field_attr(self, metric_field: MetricField) -> dict[str, Any]:
|
|
246
|
+
attrs = super().get_serializer_field_attr(metric_field)
|
|
247
|
+
pivot_date = self.val_date
|
|
248
|
+
if not pivot_date:
|
|
249
|
+
pivot_date = (date.today() - pd.tseries.offsets.BDay(1)).date()
|
|
250
|
+
if "previous" in metric_field.key:
|
|
251
|
+
pivot_date = Dataloader.get_performance_date_map(pivot_date)[metric_field.key.replace("previous_", "")]
|
|
252
|
+
|
|
253
|
+
with suppress(KeyError):
|
|
254
|
+
start_date = Dataloader.get_performance_date_map(pivot_date)[metric_field.key.replace("previous_", "")]
|
|
255
|
+
attrs[
|
|
256
|
+
"help_text"
|
|
257
|
+
] = f"The {metric_field.label} performance is computed from {start_date:%Y-%m-%d} to {pivot_date:%Y-%m-%d}"
|
|
258
|
+
return attrs
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@register(move_first=True)
|
|
262
|
+
class InstrumentPerformanceUSDMetricBackend(InstrumentPerformanceMetricBackend):
|
|
263
|
+
performance = PERFORMANCE_METRIC_USD
|
|
264
|
+
keys = [PERFORMANCE_METRIC_USD]
|
|
265
|
+
TARGET_CURRENCY_KEY = "USD"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Generator
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from django.db.models import Avg, Sum
|
|
7
|
+
from wbcore.serializers.fields.number import DisplayMode
|
|
8
|
+
from wbfdm.enums import Financial, PeriodType, SeriesType
|
|
9
|
+
from wbfdm.models import Instrument, InstrumentPrice
|
|
10
|
+
|
|
11
|
+
from ..decorators import register
|
|
12
|
+
from ..dto import Metric, MetricField, MetricKey
|
|
13
|
+
from ..exceptions import MetricInvalidParameterException
|
|
14
|
+
from .base import BaseDataloader, InstrumentMetricBaseBackend
|
|
15
|
+
|
|
16
|
+
STATISTICS_METRIC = MetricKey(
|
|
17
|
+
key="statistic",
|
|
18
|
+
label="Statistic",
|
|
19
|
+
subfields=[
|
|
20
|
+
MetricField(
|
|
21
|
+
key="revenue_y_1",
|
|
22
|
+
label="Revenue Y-1",
|
|
23
|
+
aggregate=Sum,
|
|
24
|
+
list_display_kwargs={"show": "open"},
|
|
25
|
+
decorators=[{"position": "left", "value": "{{currency_symbol}}"}],
|
|
26
|
+
serializer_kwargs={"display_mode": DisplayMode.SHORTENED},
|
|
27
|
+
),
|
|
28
|
+
MetricField(
|
|
29
|
+
key="revenue_y0",
|
|
30
|
+
label="Revenue Y0",
|
|
31
|
+
aggregate=Sum,
|
|
32
|
+
decorators=[{"position": "left", "value": "{{currency_symbol}}"}],
|
|
33
|
+
serializer_kwargs={"display_mode": DisplayMode.SHORTENED},
|
|
34
|
+
),
|
|
35
|
+
MetricField(
|
|
36
|
+
key="revenue_y1",
|
|
37
|
+
label="Revenue Y1",
|
|
38
|
+
aggregate=Sum,
|
|
39
|
+
list_display_kwargs={"show": "open"},
|
|
40
|
+
decorators=[{"position": "left", "value": "{{currency_symbol}}"}],
|
|
41
|
+
serializer_kwargs={"display_mode": DisplayMode.SHORTENED},
|
|
42
|
+
),
|
|
43
|
+
MetricField(
|
|
44
|
+
key="market_capitalization",
|
|
45
|
+
label="Market Capitalization",
|
|
46
|
+
aggregate=Sum,
|
|
47
|
+
decorators=[{"position": "left", "value": "{{currency_symbol}}"}],
|
|
48
|
+
serializer_kwargs={"display_mode": DisplayMode.SHORTENED},
|
|
49
|
+
),
|
|
50
|
+
MetricField(
|
|
51
|
+
key="price",
|
|
52
|
+
label="Price",
|
|
53
|
+
aggregate=Avg,
|
|
54
|
+
decorators=[{"position": "left", "value": "{{currency_symbol}}"}],
|
|
55
|
+
),
|
|
56
|
+
MetricField(
|
|
57
|
+
key="volume_50d",
|
|
58
|
+
label="Volume 50D",
|
|
59
|
+
aggregate=Avg,
|
|
60
|
+
serializer_kwargs={"display_mode": DisplayMode.SHORTENED},
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
STATISTICS_METRIC_USD = MetricKey(
|
|
66
|
+
key="statistic_usd",
|
|
67
|
+
label=STATISTICS_METRIC.label,
|
|
68
|
+
subfields=STATISTICS_METRIC.subfields,
|
|
69
|
+
additional_prefixes=STATISTICS_METRIC.additional_prefixes,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Dataloader(BaseDataloader):
|
|
74
|
+
METRIC_KEY = "statistic"
|
|
75
|
+
|
|
76
|
+
def __init__(self, *args, **kwargs):
|
|
77
|
+
super().__init__(*args, **kwargs)
|
|
78
|
+
self.aggregate_callback = {
|
|
79
|
+
"revenue_y_1": "sum",
|
|
80
|
+
"revenue_y0": "sum",
|
|
81
|
+
"revenue_y1": "sum",
|
|
82
|
+
"market_capitalization": "sum",
|
|
83
|
+
"price": "mean",
|
|
84
|
+
"volume_50d": "mean",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def _compute(self) -> dict[str, float]:
|
|
88
|
+
"""
|
|
89
|
+
Compute/fetch the statistics metrics. If the basket is constituted of multiple instrument, take the average of each performance
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The metrics as dictionary
|
|
93
|
+
"""
|
|
94
|
+
if self.val_date:
|
|
95
|
+
pivot_year = self.val_date.year
|
|
96
|
+
instruments = self.basket_objects
|
|
97
|
+
df_revenue = pd.DataFrame(
|
|
98
|
+
instruments.dl.financials(
|
|
99
|
+
values=[Financial.REVENUE],
|
|
100
|
+
series_type=SeriesType.COMPLETE,
|
|
101
|
+
period_type=PeriodType.ANNUAL,
|
|
102
|
+
from_year=pivot_year - 1,
|
|
103
|
+
to_year=pivot_year + 1,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if not df_revenue.empty:
|
|
107
|
+
df_revenue = (
|
|
108
|
+
df_revenue.pivot_table(index="instrument_id", columns="year", values="value")
|
|
109
|
+
.rename(
|
|
110
|
+
columns={pivot_year - 1: "revenue_y_1", pivot_year: "revenue_y0", pivot_year + 1: "revenue_y1"}
|
|
111
|
+
)
|
|
112
|
+
.astype(float)
|
|
113
|
+
)
|
|
114
|
+
df_price = (
|
|
115
|
+
pd.DataFrame(
|
|
116
|
+
InstrumentPrice.objects.filter(instrument__in=instruments, calculated=False, date=self.val_date)
|
|
117
|
+
.annotate(fx_rate=self.fx_rate_expression)
|
|
118
|
+
.annotate_market_data()
|
|
119
|
+
.values_list(
|
|
120
|
+
"instrument",
|
|
121
|
+
"internal_market_capitalization",
|
|
122
|
+
"net_value",
|
|
123
|
+
"volume_50d",
|
|
124
|
+
"fx_rate",
|
|
125
|
+
),
|
|
126
|
+
columns=[
|
|
127
|
+
"instrument",
|
|
128
|
+
"market_capitalization",
|
|
129
|
+
"price",
|
|
130
|
+
"volume_50d",
|
|
131
|
+
"fx_rate",
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
.set_index("instrument")
|
|
135
|
+
.astype(float)
|
|
136
|
+
)
|
|
137
|
+
fx_rate = df_price["fx_rate"]
|
|
138
|
+
df = pd.concat([df_revenue, df_price.drop("fx_rate", axis=1)], axis=1)
|
|
139
|
+
for key in ["revenue_y_1", "revenue_y0", "revenue_y1", "market_capitalization", "price"]:
|
|
140
|
+
if key in df.columns:
|
|
141
|
+
df[key] = df[key] / fx_rate
|
|
142
|
+
|
|
143
|
+
if not df.empty:
|
|
144
|
+
return (
|
|
145
|
+
df.reset_index()
|
|
146
|
+
.agg({k: v for k, v in self.aggregate_callback.items() if k in df.columns})
|
|
147
|
+
.dropna()
|
|
148
|
+
.to_dict()
|
|
149
|
+
)
|
|
150
|
+
return dict()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@register(move_first=True)
|
|
154
|
+
class InstrumentFinancialStatisticsMetricBackend(InstrumentMetricBaseBackend):
|
|
155
|
+
statistic = STATISTICS_METRIC
|
|
156
|
+
keys = [STATISTICS_METRIC]
|
|
157
|
+
|
|
158
|
+
def compute_metrics(self, basket: Instrument) -> Generator[Metric, None, None]:
|
|
159
|
+
val_date = self._get_valid_date(basket)
|
|
160
|
+
metrics = Dataloader(basket, val_date, target_currency_key=self.TARGET_CURRENCY_KEY).compute()
|
|
161
|
+
yield Metric(
|
|
162
|
+
metrics=metrics,
|
|
163
|
+
basket_id=basket.id,
|
|
164
|
+
basket_content_type_id=self.content_type.id,
|
|
165
|
+
key=self.statistic.key,
|
|
166
|
+
date=None,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _get_valid_date(self, instrument: Instrument) -> date:
|
|
170
|
+
if self.val_date is None and instrument.last_valuation_date:
|
|
171
|
+
return instrument.last_valuation_date
|
|
172
|
+
elif self.val_date:
|
|
173
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
174
|
+
return instrument.valuations.filter(date__lte=self.val_date).latest("date").date
|
|
175
|
+
raise MetricInvalidParameterException()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@register(move_first=True)
|
|
179
|
+
class InstrumentFinancialStatisticsUSDMetricBackend(InstrumentFinancialStatisticsMetricBackend):
|
|
180
|
+
statistic = STATISTICS_METRIC_USD
|
|
181
|
+
keys = [STATISTICS_METRIC_USD]
|
|
182
|
+
TARGET_CURRENCY_KEY = "USD"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def register(move_first: bool = False, override_backend: bool = False):
|
|
2
|
+
"""
|
|
3
|
+
Decorator to register the metric backend
|
|
4
|
+
"""
|
|
5
|
+
from wbfdm.contrib.metric.registry import backend_registry
|
|
6
|
+
|
|
7
|
+
def _model_wrapper(backend):
|
|
8
|
+
for key in backend.keys:
|
|
9
|
+
backend_registry.set(
|
|
10
|
+
key, backend.BASKET_MODEL_CLASS, backend, move_first=move_first, override_backend=override_backend
|
|
11
|
+
)
|
|
12
|
+
return backend
|
|
13
|
+
|
|
14
|
+
return _model_wrapper
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_metrics(val_date: date, key: str | None = None, basket: Any | None = None, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Compute and process metrics for a given date using the MetricOrchestrator.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
val_date (date): The validation date for the metrics computation.
|
|
12
|
+
key (Optional[str]): The optional metric backend key to narrow down the set of backends to use. Defaults to None.
|
|
13
|
+
basket (Optional[Any]): An optional basket to narrow down the backend queryset. Defaults to None.
|
|
14
|
+
**kwargs: Additional keyword arguments to pass to the MetricOrchestrator.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
None
|
|
18
|
+
"""
|
|
19
|
+
from wbfdm.contrib.metric.orchestrators import MetricOrchestrator
|
|
20
|
+
|
|
21
|
+
with suppress(KeyError):
|
|
22
|
+
orchestrator = MetricOrchestrator(val_date, key=key, basket=basket, **kwargs)
|
|
23
|
+
orchestrator.process()
|