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,77 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from django.db import models
|
|
7
|
+
from pandas.tseries.offsets import BDay
|
|
8
|
+
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
|
+
|
|
10
|
+
from .mixin import DataBackendMixin
|
|
11
|
+
from .utils import Controller
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAPPING = {
|
|
14
|
+
"MV": "market_capitalization",
|
|
15
|
+
"MVC": "market_capitalization_consolidated",
|
|
16
|
+
"VO": "volume",
|
|
17
|
+
"MAV#(X(VO),-50D,R)": "volume_50d",
|
|
18
|
+
"BETA": "beta",
|
|
19
|
+
"NOSH": "outstanding_shares",
|
|
20
|
+
"IBNOSH": "outstanding_shares_consolidated",
|
|
21
|
+
"P": "close",
|
|
22
|
+
"PI": "price_index",
|
|
23
|
+
"RY": "yield_redemption",
|
|
24
|
+
"NR": "net_return",
|
|
25
|
+
"IO": "offered_rate",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
MUTUAL_FUND_TIMEDELTA_DAY_SHIFT = 7
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register("Instrument Price", provider_key="refinitiv", save_data_in_import_source=False, passive_only=False)
|
|
32
|
+
class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
33
|
+
def __init__(self, import_credential: Optional[models.Model] = None, **kwargs):
|
|
34
|
+
self.controller = Controller(import_credential.username, import_credential.password)
|
|
35
|
+
|
|
36
|
+
def get_files(
|
|
37
|
+
self,
|
|
38
|
+
execution_time: datetime,
|
|
39
|
+
obj_external_ids: list[str] = None,
|
|
40
|
+
**kwargs,
|
|
41
|
+
) -> BytesIO:
|
|
42
|
+
execution_date = execution_time.date()
|
|
43
|
+
|
|
44
|
+
start = kwargs.get("start", (execution_date - BDay(2)).date())
|
|
45
|
+
|
|
46
|
+
delayed_instrument_ids = list(
|
|
47
|
+
filter(
|
|
48
|
+
lambda x: True,
|
|
49
|
+
obj_external_ids,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
obj_external_ids = list(filter(lambda v: v not in delayed_instrument_ids, obj_external_ids))
|
|
53
|
+
# we get all active instruments even these were we are not suppose to fetch prices.
|
|
54
|
+
fields = list(DEFAULT_MAPPING.keys())
|
|
55
|
+
if obj_external_ids or delayed_instrument_ids:
|
|
56
|
+
df_list = []
|
|
57
|
+
if obj_external_ids:
|
|
58
|
+
df_list.append(
|
|
59
|
+
self.controller.get_data(obj_external_ids, fields, start, execution_date, ibes_fields=["IBNOSH"])
|
|
60
|
+
)
|
|
61
|
+
if delayed_instrument_ids:
|
|
62
|
+
# we need to get mutual fund price in a different batch because there is a price delay and the windows approach can't handle it otherwise
|
|
63
|
+
df_list.append(
|
|
64
|
+
self.controller.get_data(
|
|
65
|
+
delayed_instrument_ids,
|
|
66
|
+
fields,
|
|
67
|
+
start - BDay(MUTUAL_FUND_TIMEDELTA_DAY_SHIFT),
|
|
68
|
+
execution_date,
|
|
69
|
+
ibes_fields=["IBNOSH"],
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
df = pd.concat(df_list, axis=0, ignore_index=True)
|
|
73
|
+
if not df.empty:
|
|
74
|
+
content_file = BytesIO()
|
|
75
|
+
df.to_json(content_file, orient="records")
|
|
76
|
+
file_name = f"instrument_price_{start:%Y-%m-%d}-{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
|
|
77
|
+
yield file_name, content_file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from wbfdm.models.instruments import Instrument
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataBackendMixin:
|
|
8
|
+
def is_object_valid(self, obj: models.Model) -> bool:
|
|
9
|
+
return (
|
|
10
|
+
super().is_object_valid(obj)
|
|
11
|
+
and obj.is_active_at_date(date.today())
|
|
12
|
+
and (obj.refinitiv_identifier_code or obj.isin or obj.refinitiv_mnemonic_code)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def get_default_queryset(self):
|
|
16
|
+
privates_equities = Instrument.objects.filter(instrument_type__key="private_equity")
|
|
17
|
+
return Instrument.objects.exclude(id__in=privates_equities.values("id"))
|
|
18
|
+
|
|
19
|
+
def get_provider_id(self, obj: models.Model) -> str:
|
|
20
|
+
if perm_id := self.controller.fetch_perm_id(
|
|
21
|
+
instrument_ric=obj.refinitiv_identifier_code,
|
|
22
|
+
instrument_isin=obj.isin,
|
|
23
|
+
instrument_mnemonic=obj.refinitiv_mnemonic_code,
|
|
24
|
+
):
|
|
25
|
+
return perm_id
|
|
26
|
+
elif (
|
|
27
|
+
obj.refinitiv_identifier_code
|
|
28
|
+
): # We default to the RIC in case permID can't be found because some instrument don't have any
|
|
29
|
+
return obj.refinitiv_identifier_code
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .controller import Controller
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from pandas.tseries.offsets import QuarterEnd, YearEnd
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
from wbcore.contrib.io.models import ImportedObjectProviderRelationship, Provider
|
|
9
|
+
from wbfdm.contrib.dsws.client import Client
|
|
10
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
11
|
+
|
|
12
|
+
REPORT_TYPE_MAP = {"QTR": "Q", "TRI": "4M", "SAN": "6M", "ANN": "Y"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Controller:
|
|
16
|
+
def __init__(self, client_username: str, client_password: str):
|
|
17
|
+
self.provider = Provider.objects.get(key="refinitiv")
|
|
18
|
+
self.client = Client(username=client_username, password=client_password)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _wrap_identifier_in_bracklet(cls, perm_ids: list[str]) -> list[str]:
|
|
22
|
+
return [f"<{perm_id}>" for perm_id in perm_ids]
|
|
23
|
+
|
|
24
|
+
def get_start_and_end_date_for_interim(self, perm_id: str, end: date, start: date | None = None) -> date | None:
|
|
25
|
+
if not start:
|
|
26
|
+
start = end - QuarterEnd(1)
|
|
27
|
+
if rel := ImportedObjectProviderRelationship.objects.filter(
|
|
28
|
+
provider=self.provider,
|
|
29
|
+
provider_identifier=perm_id,
|
|
30
|
+
content_type=ContentType.objects.get_for_model(Instrument),
|
|
31
|
+
).first():
|
|
32
|
+
instrument = rel.content_object
|
|
33
|
+
fundamentals = instrument.fundamentals.order_by("-period__period_year", "-period__period_index")
|
|
34
|
+
# Not an historical import, it's a bulk one, we need to get the next fiscal period to fetch
|
|
35
|
+
if fundamentals.exists() and (fiscal_period := fundamentals.first().period):
|
|
36
|
+
start = fiscal_period.calendar_period.end_time.date()
|
|
37
|
+
|
|
38
|
+
return (start + QuarterEnd(0)).date(), (
|
|
39
|
+
end + QuarterEnd(0) + QuarterEnd(1)
|
|
40
|
+
).date() # We always refetch since the beginning of the quarter
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_start_date_for_annual(cls, end: date, start: date | None = None) -> date:
|
|
44
|
+
if not start:
|
|
45
|
+
start = end
|
|
46
|
+
return (start - YearEnd(1)).date(), (
|
|
47
|
+
end + YearEnd(0)
|
|
48
|
+
).date() # We always refetch since the beginning of the quarter
|
|
49
|
+
|
|
50
|
+
def get_frequency(self, instrument: str) -> str:
|
|
51
|
+
try:
|
|
52
|
+
res = self.client.get_static_df([instrument], ["IBEFPD"])
|
|
53
|
+
if not res.empty:
|
|
54
|
+
return REPORT_TYPE_MAP[res.set_index("Instrument").loc[instrument, "IBEFPD"]]
|
|
55
|
+
except Exception:
|
|
56
|
+
return "Q"
|
|
57
|
+
|
|
58
|
+
def fetch_perm_id(
|
|
59
|
+
self,
|
|
60
|
+
instrument_ric: str = None,
|
|
61
|
+
instrument_isin: str = None,
|
|
62
|
+
instrument_mnemonic: str = None,
|
|
63
|
+
perm_id_symbols: list[str] = ["QPID", "IPID"],
|
|
64
|
+
) -> str | None:
|
|
65
|
+
def _process_ticker(ticker):
|
|
66
|
+
if not (df := self.client.get_static_df(tickers=[ticker], fields=perm_id_symbols)).empty:
|
|
67
|
+
for perm_id in perm_id_symbols:
|
|
68
|
+
if identifier := df[perm_id].iloc[0]:
|
|
69
|
+
return identifier
|
|
70
|
+
|
|
71
|
+
res = None
|
|
72
|
+
if instrument_ric:
|
|
73
|
+
res = _process_ticker(f"<{instrument_ric}>")
|
|
74
|
+
if instrument_mnemonic and not res:
|
|
75
|
+
res = _process_ticker(instrument_mnemonic)
|
|
76
|
+
if instrument_isin and not res:
|
|
77
|
+
res = _process_ticker(instrument_isin)
|
|
78
|
+
return res
|
|
79
|
+
|
|
80
|
+
def get_data(
|
|
81
|
+
self,
|
|
82
|
+
identifiers: list[str],
|
|
83
|
+
fields: list[str],
|
|
84
|
+
start: date | None = None,
|
|
85
|
+
end: date | None = None,
|
|
86
|
+
wrap_tickers_into_brackets: bool = True,
|
|
87
|
+
**extra_client_kwargs,
|
|
88
|
+
) -> pd.DataFrame:
|
|
89
|
+
df_list = []
|
|
90
|
+
chunked_identifiers = self.client.get_chunked_list(identifiers, len(fields))
|
|
91
|
+
|
|
92
|
+
if settings.DEBUG:
|
|
93
|
+
chunked_identifiers = tqdm(chunked_identifiers, total=len(chunked_identifiers))
|
|
94
|
+
|
|
95
|
+
frequency = extra_client_kwargs.pop("freq", "D")
|
|
96
|
+
for perm_ids in chunked_identifiers:
|
|
97
|
+
if wrap_tickers_into_brackets:
|
|
98
|
+
perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
|
|
99
|
+
if start or end:
|
|
100
|
+
dff = self.client.get_timeserie_df(
|
|
101
|
+
perm_ids, fields, start=start, end=end, freq=frequency, **extra_client_kwargs
|
|
102
|
+
)
|
|
103
|
+
df_list.append(dff)
|
|
104
|
+
else:
|
|
105
|
+
df_list.append(self.client.get_static_df(perm_ids, fields, **extra_client_kwargs))
|
|
106
|
+
if df_list:
|
|
107
|
+
return pd.concat(df_list, axis=0)
|
|
108
|
+
return pd.DataFrame()
|
|
109
|
+
|
|
110
|
+
def get_interim_fundamental_data(
|
|
111
|
+
self,
|
|
112
|
+
identifiers: list[str],
|
|
113
|
+
annual_fields: list[str],
|
|
114
|
+
initial_start: date | None = None,
|
|
115
|
+
initial_end: date | None = None,
|
|
116
|
+
wrap_tickers_into_brackets: bool = True,
|
|
117
|
+
**extra_client_kwargs,
|
|
118
|
+
) -> pd.DataFrame:
|
|
119
|
+
df_list = []
|
|
120
|
+
interim_fields_map = {field + "A" if field[0:2] == "WC" else field: field for field in annual_fields}
|
|
121
|
+
if settings.DEBUG:
|
|
122
|
+
identifiers = tqdm(identifiers, total=len(identifiers))
|
|
123
|
+
|
|
124
|
+
for instrument_perm_id in identifiers:
|
|
125
|
+
start = initial_start # copy start argument
|
|
126
|
+
end = initial_end # copy end argument
|
|
127
|
+
|
|
128
|
+
start, end = self.get_start_and_end_date_for_interim(instrument_perm_id, end, start=start)
|
|
129
|
+
if start and end:
|
|
130
|
+
perm_ids = [instrument_perm_id]
|
|
131
|
+
if wrap_tickers_into_brackets:
|
|
132
|
+
perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
|
|
133
|
+
# frequency = self.get_frequency(identifiers[0])
|
|
134
|
+
df = self.client.get_timeserie_df(
|
|
135
|
+
perm_ids,
|
|
136
|
+
list(interim_fields_map.keys()),
|
|
137
|
+
start=start,
|
|
138
|
+
end=end,
|
|
139
|
+
**extra_client_kwargs,
|
|
140
|
+
)
|
|
141
|
+
if not df.empty:
|
|
142
|
+
df_list.append(df)
|
|
143
|
+
if df_list:
|
|
144
|
+
df = pd.concat(df_list, axis=0).dropna(how="all")
|
|
145
|
+
df["period__period_interim"] = True
|
|
146
|
+
return df.rename(columns=interim_fields_map)
|
|
147
|
+
return pd.DataFrame()
|
|
148
|
+
|
|
149
|
+
def get_annual_fundamental_data(
|
|
150
|
+
self,
|
|
151
|
+
identifiers: list[str],
|
|
152
|
+
annual_fields: list[str],
|
|
153
|
+
initial_start: date | None = None,
|
|
154
|
+
initial_end: date | None = None,
|
|
155
|
+
wrap_tickers_into_brackets: bool = True,
|
|
156
|
+
**extra_client_kwargs,
|
|
157
|
+
) -> pd.DataFrame:
|
|
158
|
+
df_list = []
|
|
159
|
+
start, end = self.get_start_date_for_annual(initial_end, start=initial_start)
|
|
160
|
+
chunked_identifiers = self.client.get_chunked_list(identifiers, len(annual_fields))
|
|
161
|
+
if settings.DEBUG:
|
|
162
|
+
chunked_identifiers = tqdm(chunked_identifiers, total=len(chunked_identifiers))
|
|
163
|
+
for perm_ids in chunked_identifiers:
|
|
164
|
+
if wrap_tickers_into_brackets:
|
|
165
|
+
perm_ids = self._wrap_identifier_in_bracklet(perm_ids)
|
|
166
|
+
df = self.client.get_timeserie_df(
|
|
167
|
+
perm_ids,
|
|
168
|
+
annual_fields,
|
|
169
|
+
start=start,
|
|
170
|
+
end=end,
|
|
171
|
+
freq="Y",
|
|
172
|
+
**extra_client_kwargs,
|
|
173
|
+
)
|
|
174
|
+
if not df.empty:
|
|
175
|
+
df_list.append(df)
|
|
176
|
+
|
|
177
|
+
if df_list:
|
|
178
|
+
df = pd.concat(df_list, axis=0).dropna(how="all")
|
|
179
|
+
df["WC05200"] = 4
|
|
180
|
+
df["period__period_interim"] = False
|
|
181
|
+
return df
|
|
182
|
+
return pd.DataFrame()
|
|
File without changes
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from django.contrib.postgres.search import TrigramSimilarity
|
|
8
|
+
from django.core.exceptions import MultipleObjectsReturned
|
|
9
|
+
from django.db import IntegrityError, models
|
|
10
|
+
from django.db.models import Q
|
|
11
|
+
from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
|
|
12
|
+
from wbcore.contrib.geography.models import Geography
|
|
13
|
+
from wbcore.contrib.io.exceptions import DeserializationError
|
|
14
|
+
from wbcore.contrib.io.imports import ImportExportHandler, ImportState
|
|
15
|
+
from wbfdm.models.exchanges import Exchange
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InstrumentLookup:
|
|
19
|
+
ORDERED_KEYS = [
|
|
20
|
+
"instrument_type",
|
|
21
|
+
"isin",
|
|
22
|
+
"refinitiv_identifier_code",
|
|
23
|
+
"refinitiv_mnemonic_code",
|
|
24
|
+
"identifier",
|
|
25
|
+
"ticker",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def __init__(self, model, trigram_similarity_min_score: float = 0.8):
|
|
29
|
+
self.cache = {}
|
|
30
|
+
self.model = model
|
|
31
|
+
self.trigram_similarity_min_score = trigram_similarity_min_score
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _get_cache_key(cls, **data):
|
|
35
|
+
if data:
|
|
36
|
+
return "-".join([f"{k}:{data.get(k, None)}" for k in cls.ORDERED_KEYS if data.get(k, None) is not None])
|
|
37
|
+
|
|
38
|
+
def _lookup_instrument(
|
|
39
|
+
self,
|
|
40
|
+
instrument_type=None,
|
|
41
|
+
currency=None,
|
|
42
|
+
exchange=None,
|
|
43
|
+
name=None,
|
|
44
|
+
only_investable_universe: bool = False,
|
|
45
|
+
exact_lookup: bool = False,
|
|
46
|
+
**identifiers,
|
|
47
|
+
):
|
|
48
|
+
identifiers = {k: v for k, v in identifiers.items() if v is not None}
|
|
49
|
+
# General lookup, we try to gracefully find the instrument based on all available identifier fields
|
|
50
|
+
cache_key = self._get_cache_key(**identifiers)
|
|
51
|
+
if cache_key and cache_key in self.cache:
|
|
52
|
+
return self.cache[cache_key]
|
|
53
|
+
|
|
54
|
+
instrument = None
|
|
55
|
+
|
|
56
|
+
# We need to lookup ticker because some provider gives us ticker with or without space in it
|
|
57
|
+
if only_investable_universe:
|
|
58
|
+
instruments = self.model.objects.filter(is_investable_universe=True)
|
|
59
|
+
else:
|
|
60
|
+
instruments = self.model.objects.filter(is_security=True)
|
|
61
|
+
|
|
62
|
+
# Try exact lookup on the filtered out universe
|
|
63
|
+
for identifier_key in [
|
|
64
|
+
"isin",
|
|
65
|
+
"refinitiv_identifier_code",
|
|
66
|
+
"refinitiv_mnemonic_code",
|
|
67
|
+
"sedol",
|
|
68
|
+
"cusip",
|
|
69
|
+
"ticker",
|
|
70
|
+
"identifier",
|
|
71
|
+
]:
|
|
72
|
+
if identifier := identifiers.get(identifier_key, None):
|
|
73
|
+
with suppress(self.model.DoesNotExist, MultipleObjectsReturned):
|
|
74
|
+
identifier = str(identifier)
|
|
75
|
+
if (
|
|
76
|
+
identifier_key != "refinitiv_identifier_code"
|
|
77
|
+
): # RIC cannot be uppercased because its symbology implies meaning for lowercase characters
|
|
78
|
+
identifier = identifier.upper()
|
|
79
|
+
instrument = instruments.get(**{identifier_key: identifier})
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if not instrument and not exact_lookup:
|
|
83
|
+
if instrument_type:
|
|
84
|
+
if isinstance(instrument_type, str): # in case we receive a key as instrument type
|
|
85
|
+
instruments = instruments.filter(instrument_type__key=instrument_type)
|
|
86
|
+
else: # in case we receive a primary key as instrument type
|
|
87
|
+
instruments = instruments.filter(instrument_type=instrument_type)
|
|
88
|
+
lookup_fields = [
|
|
89
|
+
"isin",
|
|
90
|
+
"refinitiv_identifier_code",
|
|
91
|
+
"refinitiv_mnemonic_code",
|
|
92
|
+
"refinitiv_ticker",
|
|
93
|
+
"identifier",
|
|
94
|
+
"ticker",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
conditions = []
|
|
98
|
+
for field in lookup_fields:
|
|
99
|
+
if field_value := identifiers.get(field, None):
|
|
100
|
+
conditions.append(Q(**{f"{field}": field_value}))
|
|
101
|
+
if field == "isin":
|
|
102
|
+
conditions.append(Q(old_isins__contains=[field_value]))
|
|
103
|
+
if conditions:
|
|
104
|
+
instruments = instruments.filter(reduce(operator.or_, conditions))
|
|
105
|
+
if currency or exchange:
|
|
106
|
+
instruments_tmp = instruments
|
|
107
|
+
if exchange:
|
|
108
|
+
instruments_tmp = instruments_tmp.filter(exchange=exchange)
|
|
109
|
+
if currency:
|
|
110
|
+
instruments_tmp = instruments_tmp.filter(currency=currency)
|
|
111
|
+
if instruments_tmp.count() >= 1:
|
|
112
|
+
instruments = instruments_tmp
|
|
113
|
+
# last chance
|
|
114
|
+
if name and instruments.count() > 1:
|
|
115
|
+
instruments = instruments.annotate(similarity_score=TrigramSimilarity("name", name))
|
|
116
|
+
if instruments.filter(similarity_score__gt=self.trigram_similarity_min_score).count() == 1:
|
|
117
|
+
instruments = instruments.filter(similarity_score__gt=self.trigram_similarity_min_score)
|
|
118
|
+
if instruments.count() == 1:
|
|
119
|
+
instrument = instruments.first()
|
|
120
|
+
elif instrument_type and identifiers:
|
|
121
|
+
# if instrument type was provided but we still didn't find the security, we try without the instrument type in case it was mislabeled
|
|
122
|
+
instrument = self._lookup_instrument(
|
|
123
|
+
only_investable_universe=only_investable_universe,
|
|
124
|
+
exact_lookup=exact_lookup,
|
|
125
|
+
currency=currency,
|
|
126
|
+
exchange=exchange,
|
|
127
|
+
**identifiers,
|
|
128
|
+
)
|
|
129
|
+
if not instrument and name and identifiers:
|
|
130
|
+
# Sometime, identifier provided emptied the queryset of possible instruments. In a last chance approach, we try to only look for security with the given name
|
|
131
|
+
instrument = self._lookup_instrument(
|
|
132
|
+
only_investable_universe=only_investable_universe,
|
|
133
|
+
exact_lookup=exact_lookup,
|
|
134
|
+
instrument_type=instrument_type,
|
|
135
|
+
currency=currency,
|
|
136
|
+
exchange=exchange,
|
|
137
|
+
name=name,
|
|
138
|
+
)
|
|
139
|
+
if instrument:
|
|
140
|
+
self.cache[cache_key] = instrument
|
|
141
|
+
return instrument
|
|
142
|
+
|
|
143
|
+
def lookup(self, only_security: bool = False, exact_lookup: bool = False, **lookup_kwargs):
|
|
144
|
+
# To speed up lookup process, we try to get the quote from the investable universe first
|
|
145
|
+
security = self._lookup_instrument(only_investable_universe=True, exact_lookup=exact_lookup, **lookup_kwargs)
|
|
146
|
+
if not security:
|
|
147
|
+
security = self._lookup_instrument(
|
|
148
|
+
only_investable_universe=False, exact_lookup=exact_lookup, **lookup_kwargs
|
|
149
|
+
)
|
|
150
|
+
if not only_security and security:
|
|
151
|
+
quotes = security.children.all()
|
|
152
|
+
if quotes.exists():
|
|
153
|
+
# We try to find the quote for that security based on the given exchange (if provided). Otherwise, we default to the security primary exchange
|
|
154
|
+
exchange = lookup_kwargs.get("exchange")
|
|
155
|
+
if exchange and quotes.filter(exchange=exchange).exists():
|
|
156
|
+
return quotes.filter(exchange=exchange).first()
|
|
157
|
+
else:
|
|
158
|
+
return quotes.filter(is_primary=True).first()
|
|
159
|
+
return security
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class InstrumentImportHandler(ImportExportHandler):
|
|
163
|
+
MODEL_APP_LABEL: str = "wbfdm.Instrument"
|
|
164
|
+
allow_update_save_failure = True
|
|
165
|
+
exclude_update_fields = ["name", "isin", "country"]
|
|
166
|
+
|
|
167
|
+
def __init__(self, *args, **kwargs):
|
|
168
|
+
super().__init__(*args, **kwargs)
|
|
169
|
+
self.instrument_lookup = InstrumentLookup(self.model)
|
|
170
|
+
self.currency_handler = CurrencyImportHandler(self.import_source)
|
|
171
|
+
|
|
172
|
+
def _deserialize(self, data: Dict[str, Any]):
|
|
173
|
+
from wbfdm.models import Classification, InstrumentType
|
|
174
|
+
|
|
175
|
+
if isinstance(data, int):
|
|
176
|
+
data = dict(id=data)
|
|
177
|
+
if data.get("currency", None):
|
|
178
|
+
data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
|
|
179
|
+
if instrument_type := data.get("instrument_type", None):
|
|
180
|
+
if isinstance(instrument_type, str):
|
|
181
|
+
data["instrument_type"] = InstrumentType.objects.get_or_create(
|
|
182
|
+
key=instrument_type,
|
|
183
|
+
defaults={"name": instrument_type.title(), "short_name": instrument_type.title()},
|
|
184
|
+
)[0]
|
|
185
|
+
elif isinstance(instrument_type, int):
|
|
186
|
+
data["instrument_type"] = InstrumentType.objects.get(id=instrument_type)
|
|
187
|
+
if data.get("country", None):
|
|
188
|
+
data["country"] = Geography.dict_to_model(data["country"])
|
|
189
|
+
if data.get("headquarter_city", None):
|
|
190
|
+
data["headquarter_city"] = Geography.dict_to_model(data["headquarter_city"], level=Geography.Level.CITY)
|
|
191
|
+
if inception_date := data.get("inception_date", None):
|
|
192
|
+
data["inception_date"] = datetime.strptime(inception_date, "%Y-%m-%d").date()
|
|
193
|
+
if classifications := data.pop("classifications", None):
|
|
194
|
+
data["classifications"] = [Classification.dict_to_model(c) for c in classifications]
|
|
195
|
+
if (exchange_data := data.pop("exchange", None)) and isinstance(exchange_data, dict):
|
|
196
|
+
sanitized_dict = {k: v for k, v in exchange_data.items() if v is not None}
|
|
197
|
+
if sanitized_dict:
|
|
198
|
+
data["exchange"] = Exchange.dict_to_model(sanitized_dict)
|
|
199
|
+
|
|
200
|
+
def _get_instance(
|
|
201
|
+
self,
|
|
202
|
+
data: Dict[str, Any],
|
|
203
|
+
history: Optional[models.QuerySet] = None,
|
|
204
|
+
only_security: bool = False,
|
|
205
|
+
**kwargs,
|
|
206
|
+
) -> models.Model:
|
|
207
|
+
if isinstance(data, self.model):
|
|
208
|
+
return data
|
|
209
|
+
if instrument_id := data.pop("id", None):
|
|
210
|
+
try:
|
|
211
|
+
return self.model.objects.get(id=instrument_id)
|
|
212
|
+
except self.model.DoesNotExist:
|
|
213
|
+
raise DeserializationError("Instrument id does not match an existing instrument")
|
|
214
|
+
else:
|
|
215
|
+
return self.instrument_lookup.lookup(only_security=only_security, **data)
|
|
216
|
+
|
|
217
|
+
def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
|
|
218
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
219
|
+
|
|
220
|
+
if not data.get("name", None):
|
|
221
|
+
raise DeserializationError("Can't create an instrument without at least a name")
|
|
222
|
+
classifications = data.pop("classifications", None)
|
|
223
|
+
try:
|
|
224
|
+
obj = Instrument.objects.create(
|
|
225
|
+
**data,
|
|
226
|
+
import_source=self.import_source,
|
|
227
|
+
)
|
|
228
|
+
if classifications:
|
|
229
|
+
obj.classifications.set([c for c in classifications if c])
|
|
230
|
+
return obj
|
|
231
|
+
except IntegrityError:
|
|
232
|
+
self.import_source.log += f"\nError while creating new instrument with data {data}"
|
|
233
|
+
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def process_object(self, underlying_instrument_data, **kwargs):
|
|
237
|
+
if underlying_instrument_data:
|
|
238
|
+
if isinstance(underlying_instrument_data, self.model):
|
|
239
|
+
return underlying_instrument_data, ImportState.UNMODIFIED
|
|
240
|
+
if isinstance(underlying_instrument_data, int):
|
|
241
|
+
underlying_instrument_data = dict(id=underlying_instrument_data)
|
|
242
|
+
return super().process_object(
|
|
243
|
+
underlying_instrument_data,
|
|
244
|
+
include_update_fields=[
|
|
245
|
+
"isin",
|
|
246
|
+
"ticker",
|
|
247
|
+
"refinitiv_mnemonic_code",
|
|
248
|
+
"refinitiv_identifier_code",
|
|
249
|
+
"country",
|
|
250
|
+
"exchange",
|
|
251
|
+
],
|
|
252
|
+
**kwargs,
|
|
253
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth.models import Permission
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.db.models import Q
|
|
8
|
+
from slugify import slugify
|
|
9
|
+
from wbcore.contrib.authentication.models import User
|
|
10
|
+
from wbcore.contrib.io.exceptions import DeserializationError
|
|
11
|
+
from wbcore.contrib.io.imports import ImportExportHandler
|
|
12
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
13
|
+
|
|
14
|
+
from .instrument import InstrumentImportHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InstrumentListImportHandler(ImportExportHandler):
|
|
18
|
+
MODEL_APP_LABEL: str = "wbfdm.InstrumentListThroughModel"
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
self.instrument_handler = InstrumentImportHandler(self.import_source)
|
|
23
|
+
|
|
24
|
+
def _deserialize(self, data: Dict[str, Any]):
|
|
25
|
+
if from_date := data.get("from_date", None):
|
|
26
|
+
data["from_date"] = datetime.strptime(from_date, "%Y-%m-%d").date()
|
|
27
|
+
if to_date := data.get("to_date", None):
|
|
28
|
+
data["to_date"] = datetime.strptime(to_date, "%Y-%m-%d").date()
|
|
29
|
+
if instrument_list_data := data.pop("instrument_list", None):
|
|
30
|
+
if isinstance(instrument_list_data, int):
|
|
31
|
+
data["instrument_list"] = self.model.instrument_list.get_queryset().get(id=instrument_list_data)
|
|
32
|
+
elif isinstance(instrument_list_data, dict) and "name" in instrument_list_data:
|
|
33
|
+
data["instrument_list"] = self.model.instrument_list.get_queryset().get_or_create(
|
|
34
|
+
identifier=instrument_list_data.pop("identifier", slugify(instrument_list_data["name"])),
|
|
35
|
+
**instrument_list_data,
|
|
36
|
+
)[0]
|
|
37
|
+
if instrument_data := data.pop("instrument", None):
|
|
38
|
+
data["instrument"] = self.instrument_handler.process_object(
|
|
39
|
+
instrument_data, only_security=True, read_only=True
|
|
40
|
+
)[0]
|
|
41
|
+
# we try to automatically match the instrument name against a already known matched row
|
|
42
|
+
if instrument_str := data.get("instrument_str"):
|
|
43
|
+
already_existing_rows = self.model.objects.filter(
|
|
44
|
+
instrument__isnull=False,
|
|
45
|
+
instrument_str=instrument_str,
|
|
46
|
+
instrument_list=data.get("instrument_list", None),
|
|
47
|
+
)
|
|
48
|
+
if already_existing_rows.count() == 1:
|
|
49
|
+
data["instrument"] = already_existing_rows.first().instrument
|
|
50
|
+
if "instrument_list" not in data:
|
|
51
|
+
raise DeserializationError("Instrument List not find in this row")
|
|
52
|
+
|
|
53
|
+
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
|
|
54
|
+
if instrument := data.get("instrument", None):
|
|
55
|
+
return self.model.objects.filter(
|
|
56
|
+
instrument=instrument,
|
|
57
|
+
instrument_list=data["instrument_list"],
|
|
58
|
+
).first()
|
|
59
|
+
|
|
60
|
+
def _post_processing_objects(
|
|
61
|
+
self,
|
|
62
|
+
created_objs: List[models.Model],
|
|
63
|
+
modified_objs: List[models.Model],
|
|
64
|
+
unmodified_objs: List[models.Model],
|
|
65
|
+
):
|
|
66
|
+
objs = modified_objs + unmodified_objs + created_objs
|
|
67
|
+
lists = set(map(lambda x: x.instrument_list, objs))
|
|
68
|
+
leftovers_objs = self.model.objects.filter(instrument_list__in=lists)
|
|
69
|
+
for obj in objs:
|
|
70
|
+
leftovers_objs = leftovers_objs.exclude(id=obj.id)
|
|
71
|
+
leftovers_objs.delete()
|
|
72
|
+
|
|
73
|
+
instrument_dict = defaultdict(list)
|
|
74
|
+
for obj in modified_objs + created_objs:
|
|
75
|
+
instrument_dict[obj.instrument_list].append(obj)
|
|
76
|
+
for instrument_list, items in instrument_dict.items():
|
|
77
|
+
if items:
|
|
78
|
+
report = """
|
|
79
|
+
<p>List of instrument added or modified:</p>
|
|
80
|
+
<ul>
|
|
81
|
+
"""
|
|
82
|
+
for item in items:
|
|
83
|
+
if item.instrument:
|
|
84
|
+
report += f"<li>{item.instrument_str} (automatically link to {item.instrument}</li>"
|
|
85
|
+
else:
|
|
86
|
+
report += f"<li>{item.instrument_str}</li>"
|
|
87
|
+
report += "</ul>"
|
|
88
|
+
perm = Permission.objects.get(codename="administrate_instrumentlist")
|
|
89
|
+
for user in (
|
|
90
|
+
User.objects.filter(is_active=True)
|
|
91
|
+
.filter(Q(groups__permissions=perm) | Q(user_permissions=perm))
|
|
92
|
+
.distinct()
|
|
93
|
+
):
|
|
94
|
+
send_notification(
|
|
95
|
+
code="wbfdm.instrument_list_add",
|
|
96
|
+
name=f"Instruments have been added or modified into the instrument list {instrument_list.name}",
|
|
97
|
+
body=report,
|
|
98
|
+
user=user,
|
|
99
|
+
reverse_name="wbfdm:instrumentlist-detail",
|
|
100
|
+
reverse_args=[instrument_list.id],
|
|
101
|
+
)
|