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,265 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import roman
|
|
4
|
+
from django.contrib.postgres.expressions import ArraySubquery
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models.functions import Cast
|
|
7
|
+
from django.db.models.signals import post_save
|
|
8
|
+
from django.dispatch import receiver
|
|
9
|
+
from mptt.models import MPTTModel, TreeForeignKey
|
|
10
|
+
from wbcore.models import WBModel
|
|
11
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
12
|
+
|
|
13
|
+
from .instrument_relationships import InstrumentClassificationThroughModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClassificationGroup(WBModel):
|
|
17
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
18
|
+
is_primary = models.BooleanField(
|
|
19
|
+
default=False,
|
|
20
|
+
verbose_name="Primary",
|
|
21
|
+
help_text="Set to True if this " "classification must be used as " "default if not specified " "otherwise",
|
|
22
|
+
)
|
|
23
|
+
max_depth = models.IntegerField(default=0, verbose_name="Maximum Depth")
|
|
24
|
+
code_level_digits = models.IntegerField(default=2, verbose_name="The number of digits per code level")
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return f'{self.name} ({"Primary" if self.is_primary else "Non Primary"})'
|
|
28
|
+
|
|
29
|
+
def save(self, *args, **kwargs):
|
|
30
|
+
qs = ClassificationGroup.objects.filter(is_primary=True).exclude(id=self.id)
|
|
31
|
+
if self.is_primary:
|
|
32
|
+
qs.update(is_primary=False)
|
|
33
|
+
elif not qs.exists():
|
|
34
|
+
self.is_primary = True
|
|
35
|
+
return super().save(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
def get_levels_representation(self) -> List[str]:
|
|
38
|
+
return [
|
|
39
|
+
self.classifications.filter(height=i).first().level_representation
|
|
40
|
+
for i in range(self.max_depth + 1)
|
|
41
|
+
if self.classifications.filter(height=i).exists()
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
def get_fields_names(self, sep: str = "__") -> list[str]:
|
|
45
|
+
return [f"parent{f'{sep}parent' * height}" for height in range(self.max_depth)]
|
|
46
|
+
|
|
47
|
+
def annotate_queryset(
|
|
48
|
+
self,
|
|
49
|
+
queryset: models.QuerySet,
|
|
50
|
+
classification_height: int,
|
|
51
|
+
instrument_label_key: str,
|
|
52
|
+
unique: bool = False,
|
|
53
|
+
annotation_label: str = "classifications",
|
|
54
|
+
):
|
|
55
|
+
ref_id = "classification__"
|
|
56
|
+
if classification_height:
|
|
57
|
+
ref_id += f"{'parent__' * classification_height}"
|
|
58
|
+
ref_id += "id"
|
|
59
|
+
if instrument_label_key:
|
|
60
|
+
instrument_label_key += "__"
|
|
61
|
+
base_subquery = InstrumentClassificationThroughModel.objects.filter(
|
|
62
|
+
classification__group=self,
|
|
63
|
+
instrument__tree_id=models.OuterRef(f"{instrument_label_key}tree_id"),
|
|
64
|
+
instrument__lft__lte=models.OuterRef(f"{instrument_label_key}lft"),
|
|
65
|
+
instrument__rght__gte=models.OuterRef(f"{instrument_label_key}rght"),
|
|
66
|
+
)
|
|
67
|
+
if unique:
|
|
68
|
+
expression = models.Subquery(base_subquery.values(ref_id)[:1])
|
|
69
|
+
else:
|
|
70
|
+
expression = ArraySubquery(
|
|
71
|
+
base_subquery.values(ref_id).distinct(ref_id)
|
|
72
|
+
) # we use distinct in order to remove duplicated classification (e.g. classification added to the parent as well as the children)
|
|
73
|
+
|
|
74
|
+
return queryset.annotate(**{annotation_label: expression})
|
|
75
|
+
|
|
76
|
+
class Meta:
|
|
77
|
+
verbose_name = "Classification Group"
|
|
78
|
+
verbose_name_plural = "Classification Groups"
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_representation_endpoint(cls) -> str:
|
|
82
|
+
return "wbfdm:classificationgrouprepresentation-list"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_representation_value_key(cls) -> str:
|
|
86
|
+
return "id"
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_representation_label_key(cls) -> str:
|
|
90
|
+
return "{{name}}"
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_endpoint_basename(cls) -> str:
|
|
94
|
+
return "wbfdm:classificationgroup"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Classification(MPTTModel, ComplexToStringMixin):
|
|
98
|
+
parent = TreeForeignKey(
|
|
99
|
+
"self",
|
|
100
|
+
related_name="children",
|
|
101
|
+
null=True,
|
|
102
|
+
blank=True,
|
|
103
|
+
on_delete=models.CASCADE,
|
|
104
|
+
verbose_name="Parent Classification",
|
|
105
|
+
)
|
|
106
|
+
height = models.PositiveIntegerField(
|
|
107
|
+
default=0,
|
|
108
|
+
verbose_name="The height (leaf node have height 0)",
|
|
109
|
+
)
|
|
110
|
+
group = models.ForeignKey(
|
|
111
|
+
ClassificationGroup,
|
|
112
|
+
related_name="classifications",
|
|
113
|
+
on_delete=models.CASCADE,
|
|
114
|
+
verbose_name="Classification Group",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
level_representation = models.CharField(max_length=256, verbose_name="Level Representation")
|
|
118
|
+
|
|
119
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
120
|
+
code_aggregated = models.CharField(max_length=64, verbose_name="Code Aggregated")
|
|
121
|
+
|
|
122
|
+
investable = models.BooleanField(default=True, help_text="Is this classification investable for us?")
|
|
123
|
+
|
|
124
|
+
description = models.TextField(
|
|
125
|
+
default="",
|
|
126
|
+
blank=True,
|
|
127
|
+
help_text="Give a basic definition and description",
|
|
128
|
+
verbose_name="Definition/Description",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def dict_to_model(cls, classification_data):
|
|
133
|
+
if isinstance(classification_data, int):
|
|
134
|
+
return cls.objects.filter(id=classification_data).first()
|
|
135
|
+
res = cls.objects.all()
|
|
136
|
+
if code_aggregated := classification_data.get("code_aggregated", None):
|
|
137
|
+
res = res.filter(code_aggregated=code_aggregated)
|
|
138
|
+
if group_id := classification_data.get("group", None):
|
|
139
|
+
res = res.filter(group=group_id)
|
|
140
|
+
if res.count() == 1:
|
|
141
|
+
return res.first()
|
|
142
|
+
|
|
143
|
+
def __str__(self):
|
|
144
|
+
if self.computed_str:
|
|
145
|
+
return self.computed_str
|
|
146
|
+
return f"{self.code_aggregated} {self.name}"
|
|
147
|
+
|
|
148
|
+
def get_classified_instruments(self, only_favorites: bool = False) -> models.QuerySet:
|
|
149
|
+
childs_classifications = self.get_descendants(include_self=True)
|
|
150
|
+
params = {"classifications__in": childs_classifications}
|
|
151
|
+
if only_favorites:
|
|
152
|
+
params["classifications_through__is_favorite"] = True
|
|
153
|
+
|
|
154
|
+
from wbfdm.models import Instrument
|
|
155
|
+
|
|
156
|
+
return Instrument.objects.filter(**params).distinct()
|
|
157
|
+
|
|
158
|
+
def save(self, *args, **kwargs):
|
|
159
|
+
if self.parent:
|
|
160
|
+
self.group = self.parent.group
|
|
161
|
+
if not self.code_aggregated:
|
|
162
|
+
self.code_aggregated = self.get_next_valid_code(self.group, self.parent)
|
|
163
|
+
if not self.level_representation:
|
|
164
|
+
self.level_representation = self.get_default_level_representation(self.group, self.parent)
|
|
165
|
+
return super().save(*args, **kwargs)
|
|
166
|
+
|
|
167
|
+
def compute_str(self) -> str:
|
|
168
|
+
if parent := self.parent:
|
|
169
|
+
tree_titles = parent.name
|
|
170
|
+
while parent and (parent := parent.parent):
|
|
171
|
+
tree_titles += f" - {parent.name}"
|
|
172
|
+
return f"{self.code_aggregated} {self.name} [{tree_titles}] ({self.group.name})"
|
|
173
|
+
return f"{self.code_aggregated} {self.name} ({self.group.name})"
|
|
174
|
+
|
|
175
|
+
class Meta:
|
|
176
|
+
verbose_name = "Classification"
|
|
177
|
+
verbose_name_plural = "Classifications"
|
|
178
|
+
constraints = [models.UniqueConstraint(fields=["group", "code_aggregated"], name="unique_classification")]
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def get_next_valid_code(cls, group: "ClassificationGroup", parent: "Classification | None" = None) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Return the next valid classification code given the classification parent and its group parameters
|
|
184
|
+
Args:
|
|
185
|
+
group: The classification group the estimated code belongs to
|
|
186
|
+
parent: The classification parent (if any)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The next valid and unused aggregated classification code
|
|
190
|
+
"""
|
|
191
|
+
if not group:
|
|
192
|
+
raise ValueError("This method needs a group")
|
|
193
|
+
siblings_classifications = (
|
|
194
|
+
Classification.objects.filter(parent=parent, group=group)
|
|
195
|
+
.annotate(casted_code=Cast("code_aggregated", models.BigIntegerField()))
|
|
196
|
+
.order_by("-casted_code")
|
|
197
|
+
)
|
|
198
|
+
parent_level = parent.level + 1 if parent else 0
|
|
199
|
+
code_aggregated_digits = group.code_level_digits * (parent_level + 1)
|
|
200
|
+
if last_classification := siblings_classifications.first():
|
|
201
|
+
for i in range(0, 100 - last_classification.casted_code % 10**group.code_level_digits):
|
|
202
|
+
if not siblings_classifications.filter(casted_code=last_classification.casted_code + i).exists():
|
|
203
|
+
last_valid_code = last_classification.casted_code + i
|
|
204
|
+
return f"{last_valid_code:0{code_aggregated_digits}}"
|
|
205
|
+
if parent:
|
|
206
|
+
return parent.code_aggregated + f"{1:0{group.code_level_digits}}"
|
|
207
|
+
return f"{1:0{group.code_level_digits}}"
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def get_default_level_representation(
|
|
211
|
+
cls, group: "ClassificationGroup", parent: Optional["Classification"] = None
|
|
212
|
+
) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Return the default level representation, extracted from the classification siblings
|
|
215
|
+
Args:
|
|
216
|
+
group: The classification group the estimated code belongs to
|
|
217
|
+
parent: The classification parent (if any)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A default level representation (e.g. Industry)
|
|
221
|
+
"""
|
|
222
|
+
level = parent.level + 1 if parent else None
|
|
223
|
+
siblings_classifications = cls.objects.filter(level=level, group=group).order_by("id")
|
|
224
|
+
if siblings_classifications.exists():
|
|
225
|
+
return siblings_classifications.last().level_representation
|
|
226
|
+
level = roman.toRoman(level) if level else 0
|
|
227
|
+
return f"Level {level}"
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def get_representation_endpoint(cls) -> str:
|
|
231
|
+
return "wbfdm:classificationrepresentation-list"
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def get_representation_value_key(cls) -> str:
|
|
235
|
+
return "id"
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def get_endpoint_basename(cls) -> str:
|
|
239
|
+
return "wbfdm:classification"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@receiver(post_save, sender="wbfdm.Classification")
|
|
243
|
+
def post_save_parent_classification(sender, instance, created, raw, **kwargs):
|
|
244
|
+
# Recursively call the parent save function to recompute its parameters
|
|
245
|
+
if not raw and instance.parent:
|
|
246
|
+
instance.parent.save()
|
|
247
|
+
if instance.level is not None:
|
|
248
|
+
update_fields = {"height": instance.group.max_depth - instance.level}
|
|
249
|
+
Classification.objects.filter(id=instance.id).update(**update_fields)
|
|
250
|
+
instance.refresh_from_db()
|
|
251
|
+
# # Ensure initial parent classifcation span the proper classification tree structure
|
|
252
|
+
if instance.group and not instance.get_descendants().exists() and instance.level < instance.group.max_depth:
|
|
253
|
+
Classification.objects.create(
|
|
254
|
+
parent=instance,
|
|
255
|
+
group=instance.group,
|
|
256
|
+
name=f"{instance.name} (Level {instance.level})",
|
|
257
|
+
)
|
|
258
|
+
"""
|
|
259
|
+
If a parent classification is not investable, therefore its "children" become non investable too, by cascade.
|
|
260
|
+
If a child classification becomes investable and one of its parent is non investable, therefore he cannot be
|
|
261
|
+
investable as long as its parent is non investable. (-> in the serializer)
|
|
262
|
+
"""
|
|
263
|
+
if not raw and not instance.investable and instance.get_descendants().exists():
|
|
264
|
+
descandants = instance.get_descendants()
|
|
265
|
+
descandants.update(investable=False)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from slugify import slugify
|
|
3
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
4
|
+
from wbcore.models import WBModel
|
|
5
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
6
|
+
from wbfdm.import_export.handlers.instrument_list import InstrumentListImportHandler
|
|
7
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstrumentListThroughModel(ImportMixin, ComplexToStringMixin):
|
|
11
|
+
import_export_handler_class = InstrumentListImportHandler
|
|
12
|
+
"""
|
|
13
|
+
This model is not a Through model from a programming point of view, however it allows to link instrument list to
|
|
14
|
+
instruments.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
instrument_str = models.CharField(max_length=256)
|
|
18
|
+
instrument = models.ForeignKey(
|
|
19
|
+
to="wbfdm.Instrument",
|
|
20
|
+
null=True,
|
|
21
|
+
blank=True,
|
|
22
|
+
limit_choices_to=models.Q(is_security=True),
|
|
23
|
+
on_delete=models.SET_NULL,
|
|
24
|
+
)
|
|
25
|
+
instrument_list = models.ForeignKey(
|
|
26
|
+
to="wbfdm.InstrumentList",
|
|
27
|
+
on_delete=models.CASCADE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from_date = models.DateField(null=True, blank=True)
|
|
31
|
+
to_date = models.DateField(null=True, blank=True)
|
|
32
|
+
comment = models.TextField(default="", blank=True)
|
|
33
|
+
validated = models.BooleanField(default=False)
|
|
34
|
+
|
|
35
|
+
def compute_str(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Method to compute the string representation of the instance. It will save the string value to the computed_str
|
|
38
|
+
field.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The string representation of the instance.
|
|
42
|
+
"""
|
|
43
|
+
if self.instrument and self.instrument.name_repr:
|
|
44
|
+
return f"{self.instrument.name_repr} - {self.instrument_list.name}"
|
|
45
|
+
return f"{self.instrument_str} - {self.instrument_list.name}"
|
|
46
|
+
|
|
47
|
+
class Meta:
|
|
48
|
+
verbose_name = "Instrument in Instrument List"
|
|
49
|
+
constraints = [
|
|
50
|
+
models.UniqueConstraint(fields=["instrument", "instrument_list"], name="unique_instrument_per_list")
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
notification_types = [
|
|
54
|
+
(
|
|
55
|
+
"wbfdm.instrument_list_add",
|
|
56
|
+
"Instrument added to Instrument List",
|
|
57
|
+
"A notification when an instrument gets added to a list.",
|
|
58
|
+
True,
|
|
59
|
+
True,
|
|
60
|
+
True,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class InstrumentList(WBModel):
|
|
66
|
+
class InstrumentListType(models.TextChoices):
|
|
67
|
+
WATCH = "WATCH", "Watch List"
|
|
68
|
+
EXCLUSION = "EXCLUSION", "Exclusion List"
|
|
69
|
+
|
|
70
|
+
name = models.CharField(max_length=255)
|
|
71
|
+
identifier = models.CharField(max_length=255, unique=True, blank=True)
|
|
72
|
+
instrument_list_type = models.CharField(max_length=32, choices=InstrumentListType.choices, null=True, blank=True)
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return self.name
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def instruments(self) -> models.QuerySet[Instrument]:
|
|
79
|
+
"""
|
|
80
|
+
Returns a QuerySet of Instrument objects associated with the current instrument list.
|
|
81
|
+
|
|
82
|
+
This property filters the Instrument objects based on the related InstrumentListThroughModel
|
|
83
|
+
and returns only those Instruments where the foreign key is not null.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
models.QuerySet[Instrument]: A QuerySet of Instrument objects.
|
|
87
|
+
"""
|
|
88
|
+
return Instrument.objects.filter(
|
|
89
|
+
id__in=(
|
|
90
|
+
InstrumentListThroughModel.objects.filter(instrument_list=self, instrument__isnull=False).values(
|
|
91
|
+
"instrument"
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def save(self, *args, **kwargs):
|
|
97
|
+
if not self.identifier:
|
|
98
|
+
self.identifier = slugify(f"{self.name}-{self.id}")
|
|
99
|
+
super().save(*args, **kwargs)
|
|
100
|
+
|
|
101
|
+
class Meta:
|
|
102
|
+
verbose_name = "Instrument List"
|
|
103
|
+
verbose_name_plural = "Instrument Lists"
|
|
104
|
+
permissions = (("administrate_instrumentlist", "Can administrate Instrument List"),)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def get_endpoint_basename(cls):
|
|
108
|
+
return "wbfdm:instrumentlist"
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_representation_endpoint(cls):
|
|
112
|
+
return "wbfdm:instrumentlistrepresentation-list"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_representation_value_key(cls):
|
|
116
|
+
return "id"
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_representation_label_key(cls):
|
|
120
|
+
return "{{name}}"
|