wbfdm 2.2.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 +271 -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/contrib/__init__.py +0 -0
- wbfdm/contrib/dsws/__init__.py +0 -0
- wbfdm/contrib/dsws/client.py +285 -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 +245 -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/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/__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 +540 -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 +294 -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 +618 -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-2.2.1.dist-info/METADATA +15 -0
- wbfdm-2.2.1.dist-info/RECORD +337 -0
- wbfdm-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.db.models.signals import m2m_changed
|
|
4
|
+
from django.dispatch import receiver
|
|
5
|
+
from rest_framework.reverse import reverse
|
|
6
|
+
from wbcore.contrib.tags.models import TagModelMixin
|
|
7
|
+
from wbcore.models import WBModel
|
|
8
|
+
from wbcore.signals import pre_merge
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InstrumentClassificationRelatedInstrument(models.Model):
|
|
12
|
+
class Type(models.TextChoices):
|
|
13
|
+
PARTNER = "PARTNER", "Partner"
|
|
14
|
+
SUPPLIER = "SUPPLIER", "Supplier"
|
|
15
|
+
PEER = "PEER", "Peer"
|
|
16
|
+
COMPETITOR = "COMPETITOR", "Competitor"
|
|
17
|
+
BIGGEST_THREAT = "BIGGEST_THREAT", "Biggest Threat"
|
|
18
|
+
CUSTOMER = "CUSTOMER", "Customer"
|
|
19
|
+
|
|
20
|
+
related_instrument_type = models.CharField(max_length=16, choices=Type.choices, null=True, blank=True)
|
|
21
|
+
classified_instrument = models.ForeignKey(
|
|
22
|
+
to="wbfdm.InstrumentClassificationThroughModel",
|
|
23
|
+
related_name="classification_instrument_relationships",
|
|
24
|
+
on_delete=models.CASCADE,
|
|
25
|
+
)
|
|
26
|
+
related_instrument = models.ForeignKey(
|
|
27
|
+
to="wbfdm.Instrument",
|
|
28
|
+
limit_choices_to=(models.Q(instrument_type__is_classifiable=True) & models.Q(level=0)),
|
|
29
|
+
related_name="instrument_classification_related",
|
|
30
|
+
on_delete=models.CASCADE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return f"{self.classified_instrument} {self.related_instrument}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InstrumentClassificationThroughModel(TagModelMixin, models.Model):
|
|
38
|
+
instrument = models.ForeignKey(
|
|
39
|
+
"wbfdm.Instrument",
|
|
40
|
+
on_delete=models.CASCADE,
|
|
41
|
+
related_name="classifications_through",
|
|
42
|
+
limit_choices_to=(models.Q(instrument_type__is_classifiable=True) & models.Q(level=0)),
|
|
43
|
+
)
|
|
44
|
+
classification = models.ForeignKey(
|
|
45
|
+
"wbfdm.Classification",
|
|
46
|
+
on_delete=models.CASCADE,
|
|
47
|
+
related_name="instruments_through",
|
|
48
|
+
)
|
|
49
|
+
is_favorite = models.BooleanField(default=False)
|
|
50
|
+
reason = models.TextField(default="", blank=True, verbose_name="Reason for the choice")
|
|
51
|
+
pure_player = models.BooleanField(default=False, help_text="Pure Players Companies", verbose_name="Pure Player")
|
|
52
|
+
top_player = models.BooleanField(default=False, help_text="Top Players Companies", verbose_name="Top Player")
|
|
53
|
+
percent_of_revenue = models.DecimalField(
|
|
54
|
+
decimal_places=4,
|
|
55
|
+
max_digits=5,
|
|
56
|
+
null=True,
|
|
57
|
+
blank=True,
|
|
58
|
+
verbose_name="% of revenue",
|
|
59
|
+
validators=[MinValueValidator(0), MaxValueValidator(1)],
|
|
60
|
+
)
|
|
61
|
+
related_instruments = models.ManyToManyField(
|
|
62
|
+
to="wbfdm.Instrument",
|
|
63
|
+
limit_choices_to=(models.Q(instrument_type__is_classifiable=True) & models.Q(level=0)),
|
|
64
|
+
through=InstrumentClassificationRelatedInstrument,
|
|
65
|
+
through_fields=("classified_instrument", "related_instrument"),
|
|
66
|
+
blank=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_tag_detail_endpoint(self):
|
|
70
|
+
return reverse("wbfdm:classifiedinstrument-detail", [self.id])
|
|
71
|
+
|
|
72
|
+
def get_tag_representation(self):
|
|
73
|
+
return f"{self.instrument} - {self.classification}"
|
|
74
|
+
|
|
75
|
+
class Meta:
|
|
76
|
+
constraints = [
|
|
77
|
+
models.UniqueConstraint(name="unique_classifiedinstruments", fields=["instrument", "classification"])
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
return f"{self.instrument} {self.classification}"
|
|
82
|
+
|
|
83
|
+
def save(self, *args, **kwargs):
|
|
84
|
+
if self.pure_player and not self.percent_of_revenue:
|
|
85
|
+
self.percent_of_revenue = 1
|
|
86
|
+
return super().save(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_endpoint_basename(cls) -> str:
|
|
90
|
+
return "wbfdm:instrumentclassificationrelationship"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InstrumentFavoriteGroup(WBModel):
|
|
94
|
+
name = models.CharField(max_length=256)
|
|
95
|
+
instruments = models.ManyToManyField(
|
|
96
|
+
"wbfdm.Instrument",
|
|
97
|
+
related_name="favorite_groups",
|
|
98
|
+
blank=True,
|
|
99
|
+
verbose_name="Favorite Instruments Group",
|
|
100
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
101
|
+
)
|
|
102
|
+
owner = models.ForeignKey(
|
|
103
|
+
"directory.Person", on_delete=models.CASCADE, blank=True, null=True, related_name="favorite_instruments_groups"
|
|
104
|
+
)
|
|
105
|
+
public = models.BooleanField(default=False, help_text="If set to True, this group will be available to everyone.")
|
|
106
|
+
primary = models.BooleanField(
|
|
107
|
+
default=False,
|
|
108
|
+
help_text="If set to True, this group will be set as default filter for instrument based viewset (Only one primary group allowed).",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
return self.name
|
|
113
|
+
|
|
114
|
+
def save(self, *args, **kwargs):
|
|
115
|
+
if self.primary:
|
|
116
|
+
InstrumentFavoriteGroup.objects.filter(owner=self.owner, primary=True).exclude(id=self.id).update(
|
|
117
|
+
primary=False
|
|
118
|
+
)
|
|
119
|
+
return super().save(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_representation_endpoint(cls) -> str:
|
|
123
|
+
return "wbfdm:favoritegroup-list"
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def get_representation_value_key(cls) -> str:
|
|
127
|
+
return "id"
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def get_representation_label_key(cls) -> str:
|
|
131
|
+
return "{{name}} - (Public: {{public}})"
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def get_endpoint_basename(cls) -> str:
|
|
135
|
+
return "wbfdm:favoritegroup"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RelatedInstrumentThroughModel(models.Model):
|
|
139
|
+
class RelatedTypeChoices(models.TextChoices):
|
|
140
|
+
BENCHMARK = "BENCHMARK", "Benchmark"
|
|
141
|
+
PEER = "PEER", "Peer"
|
|
142
|
+
RISK_INSTRUMENT = "RISK_INSTRUMENT", "Risk Instrument"
|
|
143
|
+
|
|
144
|
+
instrument = models.ForeignKey(
|
|
145
|
+
"wbfdm.Instrument",
|
|
146
|
+
on_delete=models.CASCADE,
|
|
147
|
+
related_name="related_instruments_through",
|
|
148
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
149
|
+
)
|
|
150
|
+
related_instrument = models.ForeignKey(
|
|
151
|
+
"wbfdm.Instrument",
|
|
152
|
+
on_delete=models.CASCADE,
|
|
153
|
+
related_name="dependent_instruments_through",
|
|
154
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
155
|
+
)
|
|
156
|
+
is_primary = models.BooleanField(default=False)
|
|
157
|
+
related_type = models.CharField(
|
|
158
|
+
max_length=32, default=RelatedTypeChoices.BENCHMARK, choices=RelatedTypeChoices.choices
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
class Meta:
|
|
162
|
+
unique_together = ("instrument", "related_instrument", "is_primary", "related_type")
|
|
163
|
+
|
|
164
|
+
def save(self, *args, **kwargs):
|
|
165
|
+
qs = RelatedInstrumentThroughModel.objects.filter(
|
|
166
|
+
instrument=self.instrument, related_type=self.related_type, is_primary=True
|
|
167
|
+
).exclude(id=self.id)
|
|
168
|
+
if self.is_primary:
|
|
169
|
+
qs.update(is_primary=False)
|
|
170
|
+
elif not qs.exists():
|
|
171
|
+
self.is_primary = True
|
|
172
|
+
return super().save(*args, **kwargs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@receiver(m2m_changed, sender="wbfdm.RelatedInstrumentThroughModel")
|
|
176
|
+
def add_related_instrument(sender, instance, action, pk_set, **kwargs):
|
|
177
|
+
if action == "post_add" and pk_set:
|
|
178
|
+
for related_instrument_id in pk_set:
|
|
179
|
+
through = RelatedInstrumentThroughModel.objects.filter(
|
|
180
|
+
instrument=instance.id, related_instrument=related_instrument_id
|
|
181
|
+
).first()
|
|
182
|
+
through.save()
|
|
183
|
+
if action == "post_remove" and pk_set:
|
|
184
|
+
qs = RelatedInstrumentThroughModel.objects.filter(instrument=instance, related_type=instance.related_type)
|
|
185
|
+
if not qs.filter(is_primary=True).exists():
|
|
186
|
+
instance = qs.first()
|
|
187
|
+
instance.is_primary = True
|
|
188
|
+
instance.save()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@receiver(m2m_changed, sender="wbfdm.InstrumentClassificationThroughModel")
|
|
192
|
+
def add_classification(sender, instance, action, pk_set, **kwargs):
|
|
193
|
+
if action == "post_add" and pk_set:
|
|
194
|
+
for classification_id in pk_set:
|
|
195
|
+
if through := InstrumentClassificationThroughModel.objects.filter(
|
|
196
|
+
instrument=instance.id, classification=classification_id
|
|
197
|
+
).first():
|
|
198
|
+
through.save()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
202
|
+
def pre_merge_instrument(sender: models.Model, merged_object, main_object, **kwargs):
|
|
203
|
+
"""
|
|
204
|
+
Reassign all merged instrument preferred classification relationship to the main instrument
|
|
205
|
+
"""
|
|
206
|
+
# For every favorite group where the merged instrument is present, we remove it and assign the main instrument instad
|
|
207
|
+
for favorite_group in InstrumentFavoriteGroup.objects.filter(instruments=merged_object):
|
|
208
|
+
favorite_group.instruments.remove(merged_object)
|
|
209
|
+
favorite_group.instruments.add(main_object)
|
|
210
|
+
|
|
211
|
+
# For all related instruments relationship of the merged instrument, we reassign them to the main instrument if they don't exist yet. The relationship is then deleted.
|
|
212
|
+
for through in RelatedInstrumentThroughModel.objects.filter(instrument=merged_object):
|
|
213
|
+
RelatedInstrumentThroughModel.objects.get_or_create(
|
|
214
|
+
instrument=main_object,
|
|
215
|
+
related_instrument=through.related_instrument,
|
|
216
|
+
is_primary=through.is_primary,
|
|
217
|
+
related_type=through.related_type,
|
|
218
|
+
)
|
|
219
|
+
through.delete()
|
|
220
|
+
# We also reassign the reverse related instrument relationship where the merged instrument is the related instrument.
|
|
221
|
+
for through in RelatedInstrumentThroughModel.objects.filter(related_instrument=merged_object):
|
|
222
|
+
RelatedInstrumentThroughModel.objects.get_or_create(
|
|
223
|
+
instrument=through.instrument,
|
|
224
|
+
related_instrument=main_object,
|
|
225
|
+
is_primary=through.is_primary,
|
|
226
|
+
related_type=through.related_type,
|
|
227
|
+
)
|
|
228
|
+
through.delete()
|
|
229
|
+
|
|
230
|
+
# For all classification relationships of the merged instrument, we reassign them to the main instrument if they don't exist yet. The relationship is then deleted.
|
|
231
|
+
for through in InstrumentClassificationThroughModel.objects.filter(instrument=merged_object):
|
|
232
|
+
new_rel, created = InstrumentClassificationThroughModel.objects.get_or_create(
|
|
233
|
+
instrument=main_object,
|
|
234
|
+
classification=through.classification,
|
|
235
|
+
defaults={
|
|
236
|
+
"is_favorite": through.is_favorite,
|
|
237
|
+
"reason": through.reason,
|
|
238
|
+
"pure_player": through.pure_player,
|
|
239
|
+
"top_player": through.top_player,
|
|
240
|
+
"percent_of_revenue": through.percent_of_revenue,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
for related_instrument in through.related_instruments.all():
|
|
244
|
+
if related_instrument not in new_rel.related_instruments.all():
|
|
245
|
+
new_rel.related_instruments.add(related_instrument)
|
|
246
|
+
through.delete()
|
|
247
|
+
|
|
248
|
+
# We also reassign the reverse classification relationship where the merged instrument is the related instrument.
|
|
249
|
+
for through in InstrumentClassificationThroughModel.objects.filter(related_instruments=merged_object):
|
|
250
|
+
through.related_instruments.remove(merged_object)
|
|
251
|
+
through.related_instruments.add(main_object)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django_fsm import FSMField, transition
|
|
5
|
+
from wbcore.contrib.authentication.models import User
|
|
6
|
+
from wbcore.contrib.currency.models import Currency
|
|
7
|
+
from wbcore.contrib.geography.models import Geography
|
|
8
|
+
from wbcore.contrib.icons import WBIcon
|
|
9
|
+
from wbcore.contrib.tags.models import Tag
|
|
10
|
+
from wbcore.enums import RequestType
|
|
11
|
+
from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
12
|
+
from wbcore.models import WBModel
|
|
13
|
+
from wbfdm.models.instruments.classifications import Classification
|
|
14
|
+
|
|
15
|
+
from .instruments import Instrument, InstrumentType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InstrumentRequest(WBModel):
|
|
19
|
+
class Status(models.TextChoices):
|
|
20
|
+
PENDING = "PENDING", "Pending"
|
|
21
|
+
APPROVED = "APPROVED", "Approved"
|
|
22
|
+
DENIED = "DENIED", "Denied"
|
|
23
|
+
DRAFT = "DRAFT", "Draft"
|
|
24
|
+
|
|
25
|
+
status = FSMField(
|
|
26
|
+
default=Status.DRAFT,
|
|
27
|
+
choices=Status.choices,
|
|
28
|
+
verbose_name="Status",
|
|
29
|
+
help_text="The Request Status (default to Pending)",
|
|
30
|
+
)
|
|
31
|
+
requester = models.ForeignKey(
|
|
32
|
+
"directory.Person",
|
|
33
|
+
related_name="instrument_requests",
|
|
34
|
+
on_delete=models.SET_NULL,
|
|
35
|
+
verbose_name="Requester",
|
|
36
|
+
null=True,
|
|
37
|
+
blank=True,
|
|
38
|
+
)
|
|
39
|
+
handler = models.ForeignKey(
|
|
40
|
+
"directory.Person",
|
|
41
|
+
related_name="handled_instrument_requests",
|
|
42
|
+
on_delete=models.SET_NULL,
|
|
43
|
+
verbose_name="Handler",
|
|
44
|
+
null=True,
|
|
45
|
+
blank=True,
|
|
46
|
+
)
|
|
47
|
+
notes = models.TextField(null=True, blank=True, verbose_name="Notes")
|
|
48
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name="Created", help_text="The request creation time")
|
|
49
|
+
instrument_data = models.JSONField(default=dict, verbose_name="Instrument Data")
|
|
50
|
+
created_instrument = models.OneToOneField(
|
|
51
|
+
"wbfdm.Instrument", on_delete=models.CASCADE, blank=True, null=True, related_name="creation_request"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __str__(self):
|
|
55
|
+
return f'Instrument Request - {self.Status[self.status].label} ({"".join([f"{k}={v}" for k, v in self.instrument_data.items()])})'
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def deserialize_instrument_data(self) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
|
59
|
+
instrument_data = self.instrument_data.copy()
|
|
60
|
+
many_to_many_data = dict()
|
|
61
|
+
if currency_id := instrument_data.get("currency", None):
|
|
62
|
+
instrument_data["currency"] = Currency.objects.filter(id=currency_id).first()
|
|
63
|
+
if country_id := instrument_data.get("country", None):
|
|
64
|
+
instrument_data["country"] = Geography.countries.filter(id=country_id).first()
|
|
65
|
+
if instrument_type_id := instrument_data.get("instrument_type", None):
|
|
66
|
+
instrument_data["instrument_type"] = InstrumentType.objects.get(id=instrument_type_id)
|
|
67
|
+
if tags_list := instrument_data.pop("tags", None):
|
|
68
|
+
many_to_many_data["tags"] = [Tag.objects.filter(id=tag_id).first() for tag_id in tags_list]
|
|
69
|
+
if classifications_list := instrument_data.pop("classifications", None):
|
|
70
|
+
many_to_many_data["classifications"] = [
|
|
71
|
+
Classification.objects.filter(id=classification_id).first()
|
|
72
|
+
for classification_id in classifications_list
|
|
73
|
+
]
|
|
74
|
+
instrument_data["is_investable_universe"] = True
|
|
75
|
+
return instrument_data, many_to_many_data
|
|
76
|
+
|
|
77
|
+
def _check_already_existing_instrument(self):
|
|
78
|
+
return (
|
|
79
|
+
(isin := self.instrument_data.get("isin", None)) and Instrument.objects.filter(isin=isin).exists()
|
|
80
|
+
) or (
|
|
81
|
+
(ric := self.instrument_data.get("refinitiv_identifier_code", None))
|
|
82
|
+
and Instrument.objects.filter(refinitiv_identifier_code=ric).exists()
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@transition(
|
|
86
|
+
field=status,
|
|
87
|
+
source=[Status.PENDING],
|
|
88
|
+
target=Status.APPROVED,
|
|
89
|
+
permission=lambda instance, user: user.has_perm("wbfdm.administrate_instrument"),
|
|
90
|
+
custom={
|
|
91
|
+
"_transition_button": ActionButton(
|
|
92
|
+
method=RequestType.PATCH,
|
|
93
|
+
color=ButtonDefaultColor.WARNING,
|
|
94
|
+
identifiers=("wbfdm:instrumentrequest",),
|
|
95
|
+
icon=WBIcon.APPROVE.icon,
|
|
96
|
+
key="approve",
|
|
97
|
+
label="Approve",
|
|
98
|
+
action_label="Approve",
|
|
99
|
+
description_fields="<p>You are sure to approve this request?</p>",
|
|
100
|
+
)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
def approve(self, by: Optional[User] = None, **kwargs):
|
|
104
|
+
deserialize_instrument_data, many_to_many_data = self.deserialize_instrument_data
|
|
105
|
+
created_instrument = Instrument.objects.create(**deserialize_instrument_data)
|
|
106
|
+
for key, items in many_to_many_data.items():
|
|
107
|
+
getattr(created_instrument, key).set(items)
|
|
108
|
+
if profile := getattr(by, "profile", None):
|
|
109
|
+
self.handler = profile
|
|
110
|
+
self.created_instrument = created_instrument
|
|
111
|
+
|
|
112
|
+
def can_approve(self):
|
|
113
|
+
if self._check_already_existing_instrument():
|
|
114
|
+
return {"non_field_errors": "An instrument already exists with the proposed identifier"}
|
|
115
|
+
|
|
116
|
+
@transition(
|
|
117
|
+
field=status,
|
|
118
|
+
source=[Status.PENDING],
|
|
119
|
+
target=Status.DENIED,
|
|
120
|
+
permission=lambda instance, user: user.has_perm("wbfdm.administrate_instrument"),
|
|
121
|
+
custom={
|
|
122
|
+
"_transition_button": ActionButton(
|
|
123
|
+
method=RequestType.PATCH,
|
|
124
|
+
color=ButtonDefaultColor.WARNING,
|
|
125
|
+
identifiers=("wbfdm:instrumentrequest",),
|
|
126
|
+
icon=WBIcon.DENY.icon,
|
|
127
|
+
key="deny",
|
|
128
|
+
label="Deny",
|
|
129
|
+
action_label="Deny",
|
|
130
|
+
description_fields="<p>You are sure to deny this request?</p>",
|
|
131
|
+
)
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
def deny(self, by: Optional[User] = None, **kwargs):
|
|
135
|
+
if profile := getattr(by, "profile", None):
|
|
136
|
+
self.handler = profile
|
|
137
|
+
|
|
138
|
+
@transition(
|
|
139
|
+
field=status,
|
|
140
|
+
source=[Status.DENIED],
|
|
141
|
+
target=Status.DRAFT,
|
|
142
|
+
custom={
|
|
143
|
+
"_transition_button": ActionButton(
|
|
144
|
+
method=RequestType.PATCH,
|
|
145
|
+
color=ButtonDefaultColor.WARNING,
|
|
146
|
+
identifiers=("wbfdm:instrumentrequest",),
|
|
147
|
+
icon=WBIcon.SEND.icon,
|
|
148
|
+
key="backtodraft",
|
|
149
|
+
label="Back To Draft",
|
|
150
|
+
action_label="Back To Draft",
|
|
151
|
+
description_fields="<p>You are sure to put this request back to draft?</p>",
|
|
152
|
+
)
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
def backtodraft(self, by: Optional[User] = None, **kwargs):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@transition(
|
|
159
|
+
field=status,
|
|
160
|
+
source=[Status.DRAFT],
|
|
161
|
+
target=Status.PENDING,
|
|
162
|
+
custom={
|
|
163
|
+
"_transition_button": ActionButton(
|
|
164
|
+
method=RequestType.PATCH,
|
|
165
|
+
color=ButtonDefaultColor.WARNING,
|
|
166
|
+
identifiers=("wbfdm:instrumentrequest",),
|
|
167
|
+
icon=WBIcon.SEND.icon,
|
|
168
|
+
key="submit",
|
|
169
|
+
label="Submit",
|
|
170
|
+
action_label="Submit",
|
|
171
|
+
description_fields="<p>You are sure to submit this request back?</p>",
|
|
172
|
+
)
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
def submit(self, by: Optional[User] = None, **kwargs):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def can_submit(self) -> Dict[str, Any]:
|
|
179
|
+
if self._check_already_existing_instrument():
|
|
180
|
+
return {"non_field_errors": "An instrument already exists with the proposed identifier"}
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_representation_endpoint(cls) -> str:
|
|
184
|
+
return "wbfdm:instrumentrequestrepresentation-list"
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def get_representation_value_key(cls) -> str:
|
|
188
|
+
return "id"
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def get_representation_label_key(cls) -> str:
|
|
192
|
+
return "Request {{id}} ({{status}})"
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def get_endpoint_basename(cls) -> str:
|
|
196
|
+
return "wbfdm:instrumentrequest"
|