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,991 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from typing import Any, Generator, Iterator, Self, TypeVar
|
|
5
|
+
|
|
6
|
+
from celery import shared_task
|
|
7
|
+
from colorfield.fields import ColorField
|
|
8
|
+
from django.contrib import admin
|
|
9
|
+
from django.contrib.contenttypes.models import ContentType
|
|
10
|
+
from django.contrib.postgres.fields import ArrayField
|
|
11
|
+
from django.contrib.postgres.indexes import GinIndex
|
|
12
|
+
from django.contrib.postgres.search import SearchVector, SearchVectorField
|
|
13
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
14
|
+
from django.db import models, transaction
|
|
15
|
+
from django.db.models import Q, Value
|
|
16
|
+
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
|
|
17
|
+
from django.dispatch import receiver
|
|
18
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
19
|
+
from mptt.models import MPTTModel, TreeForeignKey, TreeManager
|
|
20
|
+
from pandas.tseries.offsets import BDay
|
|
21
|
+
from rest_framework.reverse import reverse
|
|
22
|
+
from slugify import slugify
|
|
23
|
+
from tqdm import tqdm
|
|
24
|
+
from wbcore.content_type.utils import get_ancestors_content_type
|
|
25
|
+
from wbcore.contrib.dataloader.models import Entity
|
|
26
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
27
|
+
from wbcore.contrib.io.models import ImportedObjectProviderRelationship
|
|
28
|
+
from wbcore.contrib.notifications.utils import create_notification_type
|
|
29
|
+
from wbcore.contrib.tags.models import TagModelMixin
|
|
30
|
+
from wbcore.models import WBModel
|
|
31
|
+
from wbcore.signals import pre_merge
|
|
32
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
33
|
+
from wbfdm.analysis import TechnicalAnalysis
|
|
34
|
+
from wbfdm.contrib.internal.dataloaders.market_data import MarketDataDataloader
|
|
35
|
+
from wbfdm.contrib.metric.dispatch import compute_metrics
|
|
36
|
+
from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
|
|
37
|
+
from wbfdm.models.instruments.llm.create_instrument_news_relationships import (
|
|
38
|
+
run_company_extraction_llm,
|
|
39
|
+
)
|
|
40
|
+
from wbfdm.preferences import get_default_classification_group
|
|
41
|
+
from wbfdm.signals import (
|
|
42
|
+
add_instrument_to_investable_universe,
|
|
43
|
+
instrument_price_imported,
|
|
44
|
+
)
|
|
45
|
+
from wbnews.models import News
|
|
46
|
+
from wbnews.signals import create_news_relationships
|
|
47
|
+
|
|
48
|
+
from ...dataloaders.proxies import InstrumentDataloaderProxy
|
|
49
|
+
from .instrument_relationships import RelatedInstrumentThroughModel
|
|
50
|
+
from .mixin.instruments import InstrumentPMSMixin
|
|
51
|
+
from .querysets import InstrumentQuerySet
|
|
52
|
+
from .utils import re_bloomberg, re_isin, re_mnemonic, re_ric
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InstrumentManager(TreeManager):
|
|
56
|
+
def __init__(self, with_annotation: bool = False, *args, **kwargs):
|
|
57
|
+
self.with_annotation = with_annotation
|
|
58
|
+
super().__init__(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
def _custom_rebuild_helper(self, node, left, tree_id, nodes_to_update, level):
|
|
61
|
+
right = left + 1
|
|
62
|
+
|
|
63
|
+
for child in node.children.all():
|
|
64
|
+
right = self._custom_rebuild_helper(
|
|
65
|
+
node=child,
|
|
66
|
+
left=right,
|
|
67
|
+
tree_id=tree_id,
|
|
68
|
+
nodes_to_update=nodes_to_update,
|
|
69
|
+
level=level + 1,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
setattr(node, self._rebuild_fields["left"], left)
|
|
73
|
+
setattr(node, self._rebuild_fields["right"], right)
|
|
74
|
+
setattr(node, self._rebuild_fields["level"], level)
|
|
75
|
+
setattr(node, self._rebuild_fields["tree_id"], tree_id)
|
|
76
|
+
nodes_to_update.append(node)
|
|
77
|
+
|
|
78
|
+
return right + 1
|
|
79
|
+
|
|
80
|
+
def rebuild(self, batch_size=1000, debug: bool = False, **filters) -> None:
|
|
81
|
+
"""
|
|
82
|
+
We supercharge MPTT rebuild manager method to avoid loading all instrument into memory
|
|
83
|
+
"""
|
|
84
|
+
self._find_out_rebuild_fields()
|
|
85
|
+
|
|
86
|
+
parents = self._get_parents(**filters)
|
|
87
|
+
tree_id = filters.get("tree_id", 1)
|
|
88
|
+
nodes_to_update = []
|
|
89
|
+
if debug:
|
|
90
|
+
gen = tqdm(enumerate(parents), total=len(parents))
|
|
91
|
+
else:
|
|
92
|
+
gen = enumerate(parents)
|
|
93
|
+
for index, parent in gen:
|
|
94
|
+
self._custom_rebuild_helper(
|
|
95
|
+
node=parent,
|
|
96
|
+
left=1,
|
|
97
|
+
tree_id=tree_id + index,
|
|
98
|
+
nodes_to_update=nodes_to_update,
|
|
99
|
+
level=0,
|
|
100
|
+
)
|
|
101
|
+
if len(nodes_to_update) >= batch_size:
|
|
102
|
+
self.bulk_update(
|
|
103
|
+
nodes_to_update,
|
|
104
|
+
self._rebuild_fields.values(),
|
|
105
|
+
)
|
|
106
|
+
nodes_to_update = []
|
|
107
|
+
|
|
108
|
+
self.bulk_update(
|
|
109
|
+
nodes_to_update,
|
|
110
|
+
self._rebuild_fields.values(),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
114
|
+
qs = InstrumentQuerySet(self.model, using=self._db)
|
|
115
|
+
if self.with_annotation:
|
|
116
|
+
qs = qs.annotate_all()
|
|
117
|
+
return qs
|
|
118
|
+
|
|
119
|
+
def annotate_classification_for_group(
|
|
120
|
+
self, classification_group, classification_height: int = 0, **kwargs
|
|
121
|
+
) -> models.QuerySet:
|
|
122
|
+
return self.get_queryset().annotate_classification_for_group(
|
|
123
|
+
classification_group, classification_height=classification_height, **kwargs
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def annotate_base_data(self):
|
|
127
|
+
return self.get_queryset().annotate_base_data()
|
|
128
|
+
|
|
129
|
+
def annotate_all(self):
|
|
130
|
+
return self.get_queryset().annotate_all()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SecurityInstrumentManager(InstrumentManager):
|
|
134
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
135
|
+
return super().get_queryset().filter(is_security=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ClassifiableInstrumentManager(InstrumentManager):
|
|
139
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
140
|
+
return super().get_queryset().filter(instrument_type__is_classifiable=True, level=0)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ActiveInstrumentManager(InstrumentManager):
|
|
144
|
+
def get_queryset_at_date(self, val_date: date):
|
|
145
|
+
return (
|
|
146
|
+
super()
|
|
147
|
+
.get_queryset()
|
|
148
|
+
.filter(
|
|
149
|
+
(models.Q(delisted_date__isnull=True) | models.Q(delisted_date__gte=val_date))
|
|
150
|
+
& models.Q(inception_date__isnull=False)
|
|
151
|
+
& models.Q(inception_date__lte=val_date)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def get_queryset(self):
|
|
156
|
+
return self.get_queryset_at_date(date.today())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class InvestableUniverseManager(InstrumentManager):
|
|
160
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
161
|
+
instrument_ids = set()
|
|
162
|
+
for _, ids in add_instrument_to_investable_universe.send(sender=Instrument):
|
|
163
|
+
instrument_ids.update(ids)
|
|
164
|
+
return (
|
|
165
|
+
super()
|
|
166
|
+
.get_queryset()
|
|
167
|
+
.annotate_base_data()
|
|
168
|
+
.filter(is_investable=True)
|
|
169
|
+
.filter(
|
|
170
|
+
Q(is_investable_universe=True)
|
|
171
|
+
| Q(
|
|
172
|
+
dependent_instruments_through__isnull=False
|
|
173
|
+
) # we consider instrument that are "related" to other instrument as within the investable universe by default
|
|
174
|
+
| Q(id__in=instrument_ids)
|
|
175
|
+
| Q(is_managed=True)
|
|
176
|
+
| Q(
|
|
177
|
+
dl_parameters__market_data__path="wbfdm.contrib.internal.dataloaders.market_data.MarketDataDataloader"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class InvestableInstrumentManager(InstrumentManager):
|
|
184
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
185
|
+
return super().get_queryset().filter(children__isnull=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
SelfInstrument = TypeVar("SelfInstrument", bound="Instrument")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class InstrumentType(models.Model):
|
|
192
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
193
|
+
short_name = models.CharField(max_length=128, verbose_name="Short Name")
|
|
194
|
+
name_repr = models.CharField(max_length=128, verbose_name="Name (Representation)")
|
|
195
|
+
key = models.CharField(max_length=32, verbose_name="Key", unique=True)
|
|
196
|
+
description = models.TextField(verbose_name="Description", blank=True)
|
|
197
|
+
|
|
198
|
+
is_classifiable = models.BooleanField(default=True, verbose_name="Classifiable")
|
|
199
|
+
is_security = models.BooleanField(default=True, verbose_name="Security")
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
@property
|
|
203
|
+
def PRODUCT(cls):
|
|
204
|
+
return InstrumentType.objects.get_or_create(
|
|
205
|
+
key="product", defaults={"name": "Product", "short_name": "Product"}
|
|
206
|
+
)[0]
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
@property
|
|
210
|
+
def EQUITY(cls):
|
|
211
|
+
return InstrumentType.objects.get_or_create(key="equity", defaults={"name": "equity", "short_name": "equity"})[
|
|
212
|
+
0
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
@property
|
|
217
|
+
def INDEX(cls):
|
|
218
|
+
return InstrumentType.objects.get_or_create(key="index", defaults={"name": "Index", "short_name": "Index"})[0]
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
@property
|
|
222
|
+
def CASH(cls):
|
|
223
|
+
return InstrumentType.objects.get_or_create(key="cash", defaults={"name": "Cash", "short_name": "Cash"})[0]
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
@property
|
|
227
|
+
def CASHEQUIVALENT(cls):
|
|
228
|
+
return InstrumentType.objects.get_or_create(
|
|
229
|
+
key="cash_equivalent", defaults={"name": "Cash Equivalents", "short_name": "Cash Equivalents"}
|
|
230
|
+
)[0]
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
@property
|
|
234
|
+
def PRODUCT_GROUP(cls):
|
|
235
|
+
return InstrumentType.objects.get_or_create(
|
|
236
|
+
key="product_group", defaults={"name": "Product Group", "short_name": "Product Group"}
|
|
237
|
+
)[0]
|
|
238
|
+
|
|
239
|
+
def save(self, *args, **kwargs):
|
|
240
|
+
if not self.short_name:
|
|
241
|
+
self.short_name = self.name
|
|
242
|
+
if not self.key:
|
|
243
|
+
self.key = slugify(self.name, separator="_")
|
|
244
|
+
if not self.name_repr:
|
|
245
|
+
self.name_repr = self.name
|
|
246
|
+
super().save(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
def __str__(self) -> str:
|
|
249
|
+
return f"{self.name}"
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def get_representation_endpoint(cls):
|
|
253
|
+
return "wbfdm:instrumenttyperepresentation-list"
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def get_representation_value_key(cls):
|
|
257
|
+
return "id"
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def get_representation_label_key(cls):
|
|
261
|
+
return "{{name}}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Instrument(ComplexToStringMixin, TagModelMixin, ImportMixin, InstrumentPMSMixin, WBModel, Entity, MPTTModel):
|
|
265
|
+
COMPUTED_STR_RECOMPUTE_PERIODICALLY: bool = False
|
|
266
|
+
# COMPUTED_STR_RECOMPUTE_ON_SAVE: bool = False # I am commenting this out because we need computed str to be recomputed on save but do not know why this would be an issue
|
|
267
|
+
|
|
268
|
+
import_export_handler_class = InstrumentImportHandler
|
|
269
|
+
dl_proxy = InstrumentDataloaderProxy
|
|
270
|
+
|
|
271
|
+
parent = TreeForeignKey(
|
|
272
|
+
"self",
|
|
273
|
+
related_name="children",
|
|
274
|
+
null=True,
|
|
275
|
+
blank=True,
|
|
276
|
+
on_delete=models.CASCADE,
|
|
277
|
+
verbose_name="Parent Instrument",
|
|
278
|
+
)
|
|
279
|
+
name = models.CharField(max_length=512)
|
|
280
|
+
description = models.TextField(default="", blank=True, null=True)
|
|
281
|
+
instrument_type = models.ForeignKey(
|
|
282
|
+
"wbfdm.InstrumentType", related_name="instruments", null=True, blank=True, on_delete=models.PROTECT
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
inception_date = models.DateField(null=True, blank=True)
|
|
286
|
+
delisted_date = models.DateField(null=True, blank=True)
|
|
287
|
+
last_valuation_date = models.DateField(
|
|
288
|
+
null=True, blank=True, verbose_name="Last Valuation Date", help_text="Last Valuation Date"
|
|
289
|
+
)
|
|
290
|
+
last_price_date = models.DateField(
|
|
291
|
+
null=True, blank=True, verbose_name="Last Price Date", help_text="Last Price Date"
|
|
292
|
+
)
|
|
293
|
+
# The report date fields store the actual dates when a report happens, not the end of a period.
|
|
294
|
+
last_annual_report = models.DateField(null=True, blank=True)
|
|
295
|
+
last_interim_report = models.DateField(null=True, blank=True)
|
|
296
|
+
next_annual_report = models.DateField(null=True, blank=True)
|
|
297
|
+
next_interim_report = models.DateField(null=True, blank=True)
|
|
298
|
+
|
|
299
|
+
country = models.ForeignKey(
|
|
300
|
+
to="geography.Geography",
|
|
301
|
+
null=True,
|
|
302
|
+
blank=True,
|
|
303
|
+
limit_choices_to={"level": 1},
|
|
304
|
+
on_delete=models.SET_NULL,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
currency = models.ForeignKey(
|
|
308
|
+
to="currency.Currency",
|
|
309
|
+
null=True,
|
|
310
|
+
blank=True,
|
|
311
|
+
on_delete=models.SET_NULL,
|
|
312
|
+
)
|
|
313
|
+
exchange = models.ForeignKey(
|
|
314
|
+
to="wbfdm.Exchange", null=True, blank=True, on_delete=models.PROTECT, related_name="instruments"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
source_id = models.CharField(max_length=64, null=True, blank=True)
|
|
318
|
+
source = models.CharField(max_length=64, null=True, blank=True)
|
|
319
|
+
|
|
320
|
+
# Other fields from PMS
|
|
321
|
+
founded_year = models.IntegerField(null=True, blank=True, verbose_name="Founded Year")
|
|
322
|
+
identifier = models.CharField(
|
|
323
|
+
max_length=255,
|
|
324
|
+
verbose_name="Identifier",
|
|
325
|
+
null=True,
|
|
326
|
+
blank=True,
|
|
327
|
+
)
|
|
328
|
+
name_repr = models.CharField(max_length=255, null=True, blank=True, verbose_name="Name (Representation)")
|
|
329
|
+
last_update = models.DateTimeField(auto_now=True, blank=True, null=True)
|
|
330
|
+
alternative_names = ArrayField(models.CharField(blank=True, null=True, max_length=255), default=list, blank=True)
|
|
331
|
+
isin = models.CharField(
|
|
332
|
+
null=True,
|
|
333
|
+
blank=True,
|
|
334
|
+
max_length=12,
|
|
335
|
+
verbose_name="ISIN",
|
|
336
|
+
help_text="The ISIN provided by the bank.",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
ticker = models.CharField(
|
|
340
|
+
max_length=255,
|
|
341
|
+
verbose_name="Ticker Bloomberg",
|
|
342
|
+
help_text="The Bloomberg ticker without the exchange (e.g. AAPL)",
|
|
343
|
+
blank=True,
|
|
344
|
+
null=True,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
old_isins = ArrayField(
|
|
348
|
+
base_field=models.CharField(max_length=12),
|
|
349
|
+
default=list,
|
|
350
|
+
blank=True,
|
|
351
|
+
verbose_name="Old ISINS",
|
|
352
|
+
help_text="These old ISINs are stored for this instrument to retrieve it more easily later.",
|
|
353
|
+
)
|
|
354
|
+
sedol = models.CharField(
|
|
355
|
+
max_length=255,
|
|
356
|
+
verbose_name="SEDOL",
|
|
357
|
+
help_text="Stock Exchange Daily Official List",
|
|
358
|
+
blank=True,
|
|
359
|
+
null=True,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
valoren = models.CharField(
|
|
363
|
+
max_length=255,
|
|
364
|
+
verbose_name="Valoren Number",
|
|
365
|
+
help_text="Valoren Number",
|
|
366
|
+
blank=True,
|
|
367
|
+
null=True,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
cusip = models.CharField(
|
|
371
|
+
max_length=255,
|
|
372
|
+
verbose_name="CUSIP",
|
|
373
|
+
help_text="CUSIP",
|
|
374
|
+
blank=True,
|
|
375
|
+
null=True,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
refinitiv_ticker = models.CharField(
|
|
379
|
+
max_length=255,
|
|
380
|
+
verbose_name="Refinitiv Ticker",
|
|
381
|
+
help_text="Refinitiv Refinitiv",
|
|
382
|
+
blank=True,
|
|
383
|
+
null=True,
|
|
384
|
+
)
|
|
385
|
+
refinitiv_identifier_code = models.CharField(
|
|
386
|
+
max_length=255,
|
|
387
|
+
verbose_name="RIC",
|
|
388
|
+
help_text="Refinitiv Identifier Code",
|
|
389
|
+
blank=True,
|
|
390
|
+
null=True,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
refinitiv_mnemonic_code = models.CharField(
|
|
394
|
+
max_length=255,
|
|
395
|
+
verbose_name="Refinitiv Datastream Mnemonic Code",
|
|
396
|
+
help_text="Refinitiv Datastream Mnemonic Code",
|
|
397
|
+
blank=True,
|
|
398
|
+
null=True,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
headquarter_address = models.CharField(
|
|
402
|
+
max_length=512, blank=True, null=True, help_text="The company Headquarter address"
|
|
403
|
+
)
|
|
404
|
+
headquarter_city = models.ForeignKey(
|
|
405
|
+
"geography.Geography",
|
|
406
|
+
related_name="headquarters_of",
|
|
407
|
+
null=True,
|
|
408
|
+
blank=True,
|
|
409
|
+
on_delete=models.SET_NULL,
|
|
410
|
+
verbose_name="Headquarter City",
|
|
411
|
+
help_text="The company's headquarter city",
|
|
412
|
+
limit_choices_to={"level": 3},
|
|
413
|
+
)
|
|
414
|
+
employees = models.IntegerField(null=True, blank=True)
|
|
415
|
+
|
|
416
|
+
primary_url = models.URLField(blank=True, null=True, help_text="The Company website url")
|
|
417
|
+
additional_urls = ArrayField(models.URLField(blank=True, null=True), default=list, blank=True)
|
|
418
|
+
|
|
419
|
+
related_instruments = models.ManyToManyField(
|
|
420
|
+
"self",
|
|
421
|
+
symmetrical=False,
|
|
422
|
+
related_name="benchmarks_of",
|
|
423
|
+
through="wbfdm.RelatedInstrumentThroughModel",
|
|
424
|
+
through_fields=("instrument", "related_instrument"),
|
|
425
|
+
blank=True,
|
|
426
|
+
verbose_name="The Related Instruments",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
classifications = models.ManyToManyField(
|
|
430
|
+
"wbfdm.Classification",
|
|
431
|
+
through="wbfdm.InstrumentClassificationThroughModel",
|
|
432
|
+
limit_choices_to=models.Q(level=models.F("group__max_depth")),
|
|
433
|
+
related_name="instruments",
|
|
434
|
+
blank=True,
|
|
435
|
+
verbose_name="Classifications",
|
|
436
|
+
)
|
|
437
|
+
is_cash = models.BooleanField(default=False)
|
|
438
|
+
is_cash_equivalent = models.BooleanField(default=False)
|
|
439
|
+
issue_price = models.PositiveIntegerField(
|
|
440
|
+
default=100,
|
|
441
|
+
verbose_name="Issue Price",
|
|
442
|
+
help_text="The initial issue price that is displayed on the factsheet",
|
|
443
|
+
)
|
|
444
|
+
base_color = ColorField(
|
|
445
|
+
blank=True, null=True, max_length=64, default="#FF0000"
|
|
446
|
+
) # we need this field for pms breakdown.
|
|
447
|
+
|
|
448
|
+
is_security = models.BooleanField(default=False)
|
|
449
|
+
is_managed = models.BooleanField(default=False)
|
|
450
|
+
is_primary = models.BooleanField(null=True, blank=True)
|
|
451
|
+
is_investable_universe = models.BooleanField(
|
|
452
|
+
default=False,
|
|
453
|
+
verbose_name="In Investable Universe",
|
|
454
|
+
help_text="If True, the instrument belongs to the investable universe",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
search_vector = SearchVectorField(null=True)
|
|
458
|
+
trigram_search_vector = models.CharField(max_length=1024, null=True, blank=True)
|
|
459
|
+
|
|
460
|
+
objects = InstrumentManager()
|
|
461
|
+
annotated_objects = InstrumentManager(with_annotation=True)
|
|
462
|
+
active_objects = ActiveInstrumentManager()
|
|
463
|
+
securities = SecurityInstrumentManager()
|
|
464
|
+
classifiables = ClassifiableInstrumentManager()
|
|
465
|
+
investables = InvestableInstrumentManager()
|
|
466
|
+
investable_universe = InvestableUniverseManager()
|
|
467
|
+
|
|
468
|
+
class Meta:
|
|
469
|
+
verbose_name = "Instrument"
|
|
470
|
+
verbose_name_plural = "Instruments"
|
|
471
|
+
permissions = (("administrate_instrument", "Can administrate Instrument"),)
|
|
472
|
+
constraints = [
|
|
473
|
+
models.UniqueConstraint(fields=["source_id", "source"], name="unique_source"),
|
|
474
|
+
models.UniqueConstraint(
|
|
475
|
+
fields=["refinitiv_identifier_code"],
|
|
476
|
+
name="unique_ric",
|
|
477
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
478
|
+
),
|
|
479
|
+
models.UniqueConstraint(
|
|
480
|
+
fields=["refinitiv_mnemonic_code"],
|
|
481
|
+
name="unique_rmc",
|
|
482
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
483
|
+
),
|
|
484
|
+
models.UniqueConstraint(
|
|
485
|
+
fields=["isin"],
|
|
486
|
+
name="unique_isin",
|
|
487
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
488
|
+
),
|
|
489
|
+
models.UniqueConstraint(
|
|
490
|
+
fields=["sedol"],
|
|
491
|
+
name="unique_sedol",
|
|
492
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
493
|
+
),
|
|
494
|
+
models.UniqueConstraint(
|
|
495
|
+
fields=["valoren"],
|
|
496
|
+
name="unique_valoren",
|
|
497
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
498
|
+
),
|
|
499
|
+
models.UniqueConstraint(
|
|
500
|
+
fields=["cusip"],
|
|
501
|
+
name="unique_cusip",
|
|
502
|
+
condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
|
|
503
|
+
),
|
|
504
|
+
models.UniqueConstraint(
|
|
505
|
+
fields=["parent", "is_primary"],
|
|
506
|
+
name="unique_instrument_primary",
|
|
507
|
+
condition=models.Q(is_primary=True) & models.Q(is_managed=False),
|
|
508
|
+
),
|
|
509
|
+
]
|
|
510
|
+
indexes = [
|
|
511
|
+
models.Index(fields=["parent"], name="instrument_parent_index"),
|
|
512
|
+
models.Index(fields=["parent", "exchange", "isin"], name="instrument_children_index"),
|
|
513
|
+
models.Index(fields=["is_investable_universe"], name="instrument_investible_index"),
|
|
514
|
+
models.Index(fields=["is_security"], name="instrument_security_index"),
|
|
515
|
+
models.Index(fields=["level"], name="instrument_level_index"),
|
|
516
|
+
GinIndex(fields=["search_vector"], name="instrument_sv_gin_idx"), # type: ignore
|
|
517
|
+
GinIndex(fields=["trigram_search_vector"], opclasses=["gin_trgm_ops"], name="instrument_trigram_sv_gin_idx"), # type: ignore
|
|
518
|
+
]
|
|
519
|
+
notification_types = [
|
|
520
|
+
create_notification_type(
|
|
521
|
+
code="wbfdm.instrument.notify",
|
|
522
|
+
title="Instrument Notification",
|
|
523
|
+
help_text="Sends a notification when there is an update about an instrument",
|
|
524
|
+
)
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
def get_tag_detail_endpoint(self):
|
|
528
|
+
return reverse("wbfdm:instrument-detail", [self.id])
|
|
529
|
+
|
|
530
|
+
def get_tag_representation(self):
|
|
531
|
+
return self.computed_str
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
@admin.display(description="Is Investable")
|
|
535
|
+
def _is_investable(self):
|
|
536
|
+
return getattr(self, "is_investable", not self.children.exists())
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
@admin.display(description="Primary Benchmark")
|
|
540
|
+
def primary_benchmark(self):
|
|
541
|
+
if primary_through := RelatedInstrumentThroughModel.objects.filter(
|
|
542
|
+
instrument=self, is_primary=True, related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.BENCHMARK
|
|
543
|
+
).first():
|
|
544
|
+
return primary_through.related_instrument
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
@admin.display(description="Primary Peer")
|
|
549
|
+
def primary_peer(self):
|
|
550
|
+
if primary_through := RelatedInstrumentThroughModel.objects.filter(
|
|
551
|
+
instrument=self, is_primary=True, related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.PEER
|
|
552
|
+
).first():
|
|
553
|
+
return primary_through.related_instrument
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
@admin.display(description="Primary Risk Instrument")
|
|
558
|
+
def primary_risk_instrument(self):
|
|
559
|
+
if primary_through := RelatedInstrumentThroughModel.objects.filter(
|
|
560
|
+
instrument=self,
|
|
561
|
+
is_primary=True,
|
|
562
|
+
related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.RISK_INSTRUMENT,
|
|
563
|
+
).first():
|
|
564
|
+
return primary_through.related_instrument
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
@admin.display(description="Primary Classification")
|
|
569
|
+
def primary_classification(self):
|
|
570
|
+
if primary_classification := self.classifications.filter(group__is_primary=True).first():
|
|
571
|
+
return primary_classification
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
@admin.display(description="Favorite Classification")
|
|
575
|
+
def favorite_classification(self):
|
|
576
|
+
if favorite_classification := self.classifications.filter(group=get_default_classification_group()).first():
|
|
577
|
+
return favorite_classification
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def active(self) -> bool:
|
|
581
|
+
today = date.today()
|
|
582
|
+
return (
|
|
583
|
+
self.inception_date
|
|
584
|
+
and self.inception_date <= today
|
|
585
|
+
and (not self.delisted_date or self.delisted_date > today)
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def identifier_repr(self) -> str:
|
|
590
|
+
identifier_repr = ""
|
|
591
|
+
if self.instrument_type and self.instrument_type.key == "product":
|
|
592
|
+
# Then we prioritize ISIN over ticker
|
|
593
|
+
identifiers = ["isin", "ticker", "refinitiv_identifier_code", "refinitiv_mnemonic_code"]
|
|
594
|
+
else:
|
|
595
|
+
identifiers = ["ticker", "isin", "refinitiv_identifier_code", "refinitiv_mnemonic_code"]
|
|
596
|
+
for identifier in identifiers:
|
|
597
|
+
if v := getattr(self, identifier, None):
|
|
598
|
+
identifier_repr = v
|
|
599
|
+
break
|
|
600
|
+
return identifier_repr.replace(":", "-")
|
|
601
|
+
|
|
602
|
+
@property
|
|
603
|
+
def valuations(self):
|
|
604
|
+
try:
|
|
605
|
+
return self.prices.filter(calculated=False)
|
|
606
|
+
except (
|
|
607
|
+
ValueError
|
|
608
|
+
): # ValueError because if this property is called before the instance has a primary key, it will fail
|
|
609
|
+
return Instrument.objects.none()
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def security_instrument_type(self):
|
|
613
|
+
while not self.instrument_type.is_security and self.parent:
|
|
614
|
+
return self.parent.security_instrument_type
|
|
615
|
+
return self.instrument_type
|
|
616
|
+
|
|
617
|
+
def update_search_vectors(self):
|
|
618
|
+
names = list(map(lambda x: Value(x), filter(None, [self.name, *self.alternative_names])))
|
|
619
|
+
if names:
|
|
620
|
+
isins = list(map(lambda x: Value(x), filter(None, [self.isin, *self.old_isins])))
|
|
621
|
+
identifiers = list(
|
|
622
|
+
map(
|
|
623
|
+
lambda x: Value(re.sub(r"[^a-zA-Z0-9]", "", x.lower())),
|
|
624
|
+
filter(
|
|
625
|
+
None,
|
|
626
|
+
[
|
|
627
|
+
self.ticker,
|
|
628
|
+
self.refinitiv_identifier_code,
|
|
629
|
+
self.refinitiv_mnemonic_code,
|
|
630
|
+
self.valoren,
|
|
631
|
+
self.sedol,
|
|
632
|
+
],
|
|
633
|
+
),
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
self.search_vector = SearchVector(*names, weight="D")
|
|
637
|
+
if identifiers:
|
|
638
|
+
self.search_vector += SearchVector(*identifiers, weight="A")
|
|
639
|
+
if isins:
|
|
640
|
+
self.search_vector += SearchVector(*isins, weight="A")
|
|
641
|
+
self.trigram_search_vector = f"{self.name} {' '.join(self.alternative_names)}".strip()
|
|
642
|
+
|
|
643
|
+
def clean(self):
|
|
644
|
+
if self.is_investable_universe and self.id and self.children.exists():
|
|
645
|
+
raise ValidationError("An instrument in the investable universe cannot have children")
|
|
646
|
+
return self
|
|
647
|
+
|
|
648
|
+
def pre_save(self):
|
|
649
|
+
if self.instrument_type:
|
|
650
|
+
self.is_security = self.instrument_type.is_security
|
|
651
|
+
if self.delisted_date:
|
|
652
|
+
self.is_security = False
|
|
653
|
+
if not self.name_repr:
|
|
654
|
+
self.name_repr = self.name
|
|
655
|
+
if not self.founded_year and self.inception_date:
|
|
656
|
+
self.founded_year = self.inception_date.year
|
|
657
|
+
if not self.inception_date:
|
|
658
|
+
self.inception_date = date.today() - timedelta(days=1)
|
|
659
|
+
if not self.founded_year:
|
|
660
|
+
self.founded_year = self.inception_date.year
|
|
661
|
+
if self.level is None:
|
|
662
|
+
self.level = 0
|
|
663
|
+
self.rght = 0
|
|
664
|
+
self.lft = 0
|
|
665
|
+
self.tree_id = 0
|
|
666
|
+
|
|
667
|
+
# ensure all identifiers are stored in uppercase and do not contain any whitespace
|
|
668
|
+
for identifier_key in ["isin", "refinitiv_mnemonic_code", "ticker", "sedol", "cusip", "identifier"]:
|
|
669
|
+
if identifier := getattr(self, identifier_key, None):
|
|
670
|
+
setattr(self, identifier_key, identifier.upper().replace(" ", ""))
|
|
671
|
+
if self.refinitiv_identifier_code:
|
|
672
|
+
self.refinitiv_identifier_code = self.refinitiv_identifier_code.replace(
|
|
673
|
+
" ", ""
|
|
674
|
+
) # RIC cannot be uppercased because its symbology implies meaning for lowercase characters
|
|
675
|
+
self.update_search_vectors()
|
|
676
|
+
if self.is_primary and (parent := self.parent):
|
|
677
|
+
# we have a unique constraint on parent. We take the time to make sure no other children is already primary = True (otherwise, update will fail)
|
|
678
|
+
Instrument.objects.filter(parent=parent, is_primary=True).exclude(
|
|
679
|
+
source=self.source, source_id=self.source_id
|
|
680
|
+
).update(is_primary=False)
|
|
681
|
+
if not self.parent:
|
|
682
|
+
self.is_primary = True
|
|
683
|
+
if self.id and (not self.instrument_type or not self.currency) and self.children.count() == 1:
|
|
684
|
+
child = self.children.first()
|
|
685
|
+
if not self.instrument_type:
|
|
686
|
+
self.instrument_type = child.instrument_type
|
|
687
|
+
if not self.currency:
|
|
688
|
+
self.currency = child.currency
|
|
689
|
+
|
|
690
|
+
def save(self, *args, **kwargs):
|
|
691
|
+
self.pre_save()
|
|
692
|
+
if self.is_primary is None:
|
|
693
|
+
self.is_primary = not self.parent or (self.exchange is not None and self.parent.exchange == self.exchange)
|
|
694
|
+
super().save(*args, **kwargs)
|
|
695
|
+
|
|
696
|
+
def compute_str(self):
|
|
697
|
+
repr = self.name_repr # we follow bloomberg instrument representation format
|
|
698
|
+
if self.instrument_type:
|
|
699
|
+
repr += f" {self.instrument_type.short_name}"
|
|
700
|
+
if self.identifier_repr:
|
|
701
|
+
repr += f" - {self.identifier_repr}"
|
|
702
|
+
if self.exchange:
|
|
703
|
+
repr += f" ({str(self.exchange)})"
|
|
704
|
+
return repr
|
|
705
|
+
|
|
706
|
+
def is_active_at_date(self, today: date) -> bool:
|
|
707
|
+
return (
|
|
708
|
+
self.inception_date is not None
|
|
709
|
+
and self.inception_date <= today
|
|
710
|
+
and (not self.delisted_date or self.delisted_date > today)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def update_last_valuation_date(self):
|
|
714
|
+
if self.valuations.exists():
|
|
715
|
+
earliest_valuation = self.valuations.earliest("date")
|
|
716
|
+
last_valuation = self.valuations.latest("date").date
|
|
717
|
+
if not self.inception_date or (
|
|
718
|
+
not earliest_valuation.calculated and self.inception_date > earliest_valuation.date
|
|
719
|
+
):
|
|
720
|
+
self.inception_date = earliest_valuation.date
|
|
721
|
+
if not self.last_valuation_date or last_valuation >= self.last_valuation_date:
|
|
722
|
+
self.last_valuation_date = last_valuation
|
|
723
|
+
if self.prices.exists():
|
|
724
|
+
last_price = self.prices.latest("date").date
|
|
725
|
+
if not self.last_price_date or last_price >= self.last_price_date:
|
|
726
|
+
self.last_price_date = last_price
|
|
727
|
+
self.save()
|
|
728
|
+
compute_metrics(last_price, basket=self)
|
|
729
|
+
|
|
730
|
+
def get_prices(self, only_instrument_price: bool = False, **kwargs) -> Iterator[dict[str, any]]:
|
|
731
|
+
qs = Instrument.objects.filter(id=self.id)
|
|
732
|
+
if "market_data" in self.dl_parameters and not only_instrument_price:
|
|
733
|
+
return qs.dl.market_data(**kwargs)
|
|
734
|
+
# if not dataloader is found, we default to the internal instrument price dataloader
|
|
735
|
+
return MarketDataDataloader(qs).market_data(**kwargs)
|
|
736
|
+
|
|
737
|
+
def get_classifable_ancestor(self, include_self: bool = True) -> Self:
|
|
738
|
+
root = self.get_root()
|
|
739
|
+
if root.instrument_type and root.instrument_type.is_classifiable:
|
|
740
|
+
return root
|
|
741
|
+
|
|
742
|
+
def get_security_ancestor(self, include_self: bool = True) -> Self:
|
|
743
|
+
if include_self:
|
|
744
|
+
parent = self
|
|
745
|
+
else:
|
|
746
|
+
parent = self.parent
|
|
747
|
+
while parent:
|
|
748
|
+
if parent.instrument_type and parent.instrument_type.is_security:
|
|
749
|
+
return parent
|
|
750
|
+
parent = parent.parent
|
|
751
|
+
|
|
752
|
+
def merge(self, merged_instrument: SelfInstrument, dispatch: bool = True, override_fields_to_copy: bool = False):
|
|
753
|
+
"""
|
|
754
|
+
This method handle the deletion of an instrument ("merged_instrument") in favor of another one (self).
|
|
755
|
+
|
|
756
|
+
After handling all the instrument base logic reassignment, it calls a signal that all modules can implement in
|
|
757
|
+
order to implement their own reassignment logic.
|
|
758
|
+
|
|
759
|
+
The function is atomic, it either succeed or fail (i.e. If merged_instrument is deleted, the merge was succeesful)
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
merged_instrument: The Instrument that is supposed to be merged and deleted
|
|
763
|
+
"""
|
|
764
|
+
if self == merged_instrument:
|
|
765
|
+
return
|
|
766
|
+
with transaction.atomic(): # We want this to either succeed fully or fail
|
|
767
|
+
# Get the base
|
|
768
|
+
if dispatch:
|
|
769
|
+
pre_merge.send(
|
|
770
|
+
sender=Instrument, merged_object=merged_instrument, main_object=self
|
|
771
|
+
) # default signal dispatch for the Instrument class
|
|
772
|
+
|
|
773
|
+
# if the self type is different than Instrument, it's a polymorphic call. We fire also the pre_merge signal with this child type
|
|
774
|
+
if type(self) is not Instrument: # noqa
|
|
775
|
+
pre_merge.send(sender=self.__class__, merged_object=merged_instrument, main_object=self)
|
|
776
|
+
|
|
777
|
+
# We refresh the reference in case the underlying signal receivers modify these objects
|
|
778
|
+
self.refresh_from_db()
|
|
779
|
+
merged_instrument.refresh_from_db()
|
|
780
|
+
|
|
781
|
+
# We delete finally the merged instrument. All unlikage should have been done in the signal receivers function
|
|
782
|
+
merged_instrument.delete()
|
|
783
|
+
|
|
784
|
+
# Finally, we copy the potentially missing field from merged instrument to self (Only if self.field is None)
|
|
785
|
+
field_to_copy = [
|
|
786
|
+
"founded_year",
|
|
787
|
+
"inception_date",
|
|
788
|
+
"delisted_date",
|
|
789
|
+
"name",
|
|
790
|
+
"name_repr",
|
|
791
|
+
"description",
|
|
792
|
+
"ticker",
|
|
793
|
+
"country",
|
|
794
|
+
"headquarter_address",
|
|
795
|
+
"headquarter_city",
|
|
796
|
+
"primary_url",
|
|
797
|
+
"identifier",
|
|
798
|
+
"currency",
|
|
799
|
+
"isin",
|
|
800
|
+
"sedol",
|
|
801
|
+
"valoren",
|
|
802
|
+
"refinitiv_ticker",
|
|
803
|
+
"refinitiv_identifier_code",
|
|
804
|
+
"refinitiv_mnemonic_code",
|
|
805
|
+
"exchange",
|
|
806
|
+
"source",
|
|
807
|
+
"source_id",
|
|
808
|
+
"employees",
|
|
809
|
+
"last_annual_report",
|
|
810
|
+
"last_interim_report",
|
|
811
|
+
"next_annual_report",
|
|
812
|
+
"next_interim_report",
|
|
813
|
+
"parent",
|
|
814
|
+
]
|
|
815
|
+
many_fields = [
|
|
816
|
+
"alternative_names",
|
|
817
|
+
"old_isins",
|
|
818
|
+
"additional_urls",
|
|
819
|
+
]
|
|
820
|
+
for field in field_to_copy:
|
|
821
|
+
if (new_value := getattr(merged_instrument, field, None)) is not None and (
|
|
822
|
+
getattr(self, field, None) is None or override_fields_to_copy
|
|
823
|
+
):
|
|
824
|
+
setattr(self, field, new_value)
|
|
825
|
+
for field in many_fields:
|
|
826
|
+
current_values = getattr(self, field, [])
|
|
827
|
+
new_values = getattr(merged_instrument, field, [])
|
|
828
|
+
setattr(self, field, list(set(current_values + new_values)))
|
|
829
|
+
self.save()
|
|
830
|
+
|
|
831
|
+
def handle_backend_lookup(self, attribute: str, method: str):
|
|
832
|
+
try:
|
|
833
|
+
backend = self.lookups.get(**{attribute: True}).load_backend()
|
|
834
|
+
return getattr(backend, method)()
|
|
835
|
+
except ObjectDoesNotExist:
|
|
836
|
+
return self.__class__.objects.none()
|
|
837
|
+
|
|
838
|
+
def technical_analysis(self, from_date: date | None = None, to_date: date | None = None):
|
|
839
|
+
return TechnicalAnalysis.init_full_from_instrument(self, from_date, to_date)
|
|
840
|
+
|
|
841
|
+
def technical_benchmark_analysis(self, from_date: date | None = None, to_date: date | None = None):
|
|
842
|
+
return TechnicalAnalysis.init_close_from_instrument(self, from_date, to_date)
|
|
843
|
+
|
|
844
|
+
def import_prices(self, start: date | None = None, end: date | None = None, clear: bool = False):
|
|
845
|
+
if not self.is_leaf_node():
|
|
846
|
+
raise ValueError("Cannot import price on a non-leaf node")
|
|
847
|
+
if not start:
|
|
848
|
+
start = (
|
|
849
|
+
self.inception_date
|
|
850
|
+
if self.inception_date
|
|
851
|
+
else global_preferences_registry.manager()["wbfdm__default_start_date_historical_import"]
|
|
852
|
+
)
|
|
853
|
+
if not end:
|
|
854
|
+
end = (
|
|
855
|
+
date.today() - BDay(1)
|
|
856
|
+
).date() # we don't import today price in case the dataloader returns duplicates (e.g. DSWS)
|
|
857
|
+
|
|
858
|
+
# we detect when was the last date price imported before start and switch the start date from there
|
|
859
|
+
with suppress(ObjectDoesNotExist):
|
|
860
|
+
start = self.prices.filter(date__lte=start).latest("date").date
|
|
861
|
+
# Import instrument prices
|
|
862
|
+
self.save_prices_in_db(start, end, clear=clear)
|
|
863
|
+
|
|
864
|
+
# compute daily statistics & performances
|
|
865
|
+
self.update_last_valuation_date()
|
|
866
|
+
|
|
867
|
+
instrument_price_imported.send(sender=Instrument, instrument=self, start=start, end=end)
|
|
868
|
+
|
|
869
|
+
@classmethod
|
|
870
|
+
def parse_content_for_identifiers(cls, content: str) -> Generator[dict[str, Any], None, None]:
|
|
871
|
+
for ric in re_ric(content):
|
|
872
|
+
yield {"refinitiv_identifier_code": ric}
|
|
873
|
+
for isin in re_isin(content):
|
|
874
|
+
yield {"isin": isin}
|
|
875
|
+
for ticker in re_bloomberg(content):
|
|
876
|
+
yield {"ticker": ticker}
|
|
877
|
+
for mnemonic in re_mnemonic(content):
|
|
878
|
+
yield {"refinitiv_mnemonic_code": mnemonic}
|
|
879
|
+
|
|
880
|
+
@classmethod
|
|
881
|
+
def get_endpoint_basename(cls):
|
|
882
|
+
return "wbfdm:instrument"
|
|
883
|
+
|
|
884
|
+
@classmethod
|
|
885
|
+
def get_representation_endpoint(cls):
|
|
886
|
+
return "wbfdm:instrumentrepresentation-list"
|
|
887
|
+
|
|
888
|
+
@classmethod
|
|
889
|
+
def get_representation_value_key(cls):
|
|
890
|
+
return "id"
|
|
891
|
+
|
|
892
|
+
@classmethod
|
|
893
|
+
def get_representation_label_key(cls):
|
|
894
|
+
return "{{computed_str}}"
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@shared_task(queue="portfolio")
|
|
898
|
+
def import_prices_as_task(instrument_id, **kwargs):
|
|
899
|
+
instrument = Instrument.objects.get(id=instrument_id)
|
|
900
|
+
instrument.import_prices(**kwargs)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@receiver(pre_delete, sender="wbfdm.Instrument")
|
|
904
|
+
def pre_delete_instrument(sender, instance, **kwargs):
|
|
905
|
+
ImportedObjectProviderRelationship.objects.filter(
|
|
906
|
+
content_type__in=get_ancestors_content_type(ContentType.objects.get_for_model(instance)), object_id=instance.id
|
|
907
|
+
).delete()
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@receiver(pre_save, sender="wbfdm.Instrument")
|
|
911
|
+
def pre_save_instrument(sender, instance, raw, **kwargs):
|
|
912
|
+
if not raw:
|
|
913
|
+
pre_instance = None
|
|
914
|
+
if instance.id:
|
|
915
|
+
pre_instance = sender.objects.get(id=instance.id)
|
|
916
|
+
# Remove duplicates if existings
|
|
917
|
+
instance.old_isins = list(set(instance.old_isins))
|
|
918
|
+
if pre_instance:
|
|
919
|
+
if (
|
|
920
|
+
pre_instance.isin
|
|
921
|
+
and instance.isin
|
|
922
|
+
and pre_instance.isin != instance.isin
|
|
923
|
+
and pre_instance.isin not in instance.old_isins
|
|
924
|
+
):
|
|
925
|
+
instance.old_isins = [*instance.old_isins, pre_instance.isin]
|
|
926
|
+
if pre_instance.name_repr != instance.name_repr:
|
|
927
|
+
# if a family member get is name representation updated, we update it for the whole family
|
|
928
|
+
pre_instance.get_family().update(name_repr=instance.name_repr)
|
|
929
|
+
|
|
930
|
+
# the instrument was manually included into the investable universe, in that case, we need to fetch the price
|
|
931
|
+
if (
|
|
932
|
+
instance.is_leaf_node()
|
|
933
|
+
and (not pre_instance or not pre_instance.is_investable_universe)
|
|
934
|
+
and instance.is_investable_universe
|
|
935
|
+
):
|
|
936
|
+
import_prices_as_task.apply_async(
|
|
937
|
+
(instance.id,), {"clear": True}, countdown=15
|
|
938
|
+
) # we need to introduce a countdown to avoid racing condition where save resumed after shared task picked up its instance reference.
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@receiver(post_save, sender="wbfdm.Classification")
|
|
942
|
+
def ensure_classification_0_height(sender, instance, created, raw, **kwargs):
|
|
943
|
+
# Ensure that if a leaf classification becomes non-leaf, then all instruments linked to it are updated automatically
|
|
944
|
+
# with the new leaf classiciation
|
|
945
|
+
if not raw and instance.parent and instance.height == 0:
|
|
946
|
+
for instrument in Instrument.objects.filter(classifications=instance.parent):
|
|
947
|
+
instrument.classifications.remove(instance.parent)
|
|
948
|
+
instrument.classifications.add(instance)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@receiver(post_delete, sender="wbfdm.InstrumentPrice")
|
|
952
|
+
def post_delete_valuation(sender, instance, **kwargs):
|
|
953
|
+
if not instance.calculated and instance.instrument.last_valuation_date == instance.date:
|
|
954
|
+
instance.instrument.update_last_valuation_date()
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class CashManager(InstrumentManager):
|
|
958
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
959
|
+
return super().get_queryset().filter(instrument_type=InstrumentType.CASH)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class Cash(Instrument):
|
|
963
|
+
objects = CashManager()
|
|
964
|
+
|
|
965
|
+
class Meta:
|
|
966
|
+
proxy = True
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@receiver(post_save, sender="currency.Currency")
|
|
970
|
+
def create_cash_from_currency(sender, instance, created, raw, **kwargs):
|
|
971
|
+
if created:
|
|
972
|
+
Instrument.objects.get_or_create(
|
|
973
|
+
instrument_type=InstrumentType.CASH, currency=instance, defaults={"name": f"Cash {instance.key}"}
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
class EquityManager(InstrumentManager):
|
|
978
|
+
def get_queryset(self) -> InstrumentQuerySet:
|
|
979
|
+
return super().get_queryset().filter(instrument_type__key="equity")
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
class Equity(Instrument):
|
|
983
|
+
objects = EquityManager()
|
|
984
|
+
|
|
985
|
+
class Meta:
|
|
986
|
+
proxy = True
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
@receiver(create_news_relationships, sender="wbnews.News")
|
|
990
|
+
def get_news_relationships_for_instruments_task(sender: type, instance: "News", **kwargs) -> shared_task:
|
|
991
|
+
return run_company_extraction_llm.s(instance.title, instance.description, instance.summary)
|