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,285 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Generator, List, Optional
|
|
5
|
+
|
|
6
|
+
import DatastreamPy as dsweb
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Client:
|
|
13
|
+
MAXIMUM_ITEMS_PER_BUNDLE: int = 500
|
|
14
|
+
MAXIMUM_REQUESTS_PER_BUNDLE: int = 20
|
|
15
|
+
MAXIMUM_INSTRUMENTS_PER_REQUEST: int = 50
|
|
16
|
+
MAXIMUM_DATATYPES_PER_REQUEST: int = 50
|
|
17
|
+
MAXIMUM_ITEMS_PER_REQUEST: int = 100
|
|
18
|
+
PRICE_ERROR_MARGIN: float = 0.10
|
|
19
|
+
IBUNIT_DEFAULT_UNIT: float = 1e6
|
|
20
|
+
|
|
21
|
+
def __init__(self, username: Optional[str] = None, password: Optional[str] = None):
|
|
22
|
+
self.connection = dsweb.DataClient(None, username, password)
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_chunked_list(cls, identifiers: list[str], fields_number: int) -> list[list[str]]:
|
|
27
|
+
instruments_number = len(identifiers)
|
|
28
|
+
max_chunks_number = instruments_number * fields_number / cls.MAXIMUM_ITEMS_PER_BUNDLE
|
|
29
|
+
chunk_size = int(instruments_number / max_chunks_number) - 1
|
|
30
|
+
res = []
|
|
31
|
+
for x in range(0, len(identifiers), chunk_size):
|
|
32
|
+
res.append(identifiers[x : x + chunk_size])
|
|
33
|
+
return res
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def _breakdown_requests(
|
|
37
|
+
cls, tickers: List[str], symbols: List[str]
|
|
38
|
+
) -> Generator[tuple[list[str], list[str]], None, None]:
|
|
39
|
+
"""
|
|
40
|
+
Helper function to generate tuple of ticker and symbols that respect the API data usage
|
|
41
|
+
Args:
|
|
42
|
+
tickers: List of tickers to fetch
|
|
43
|
+
symbols: Corresponding list of symbol to fetch
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Yield tuple of list of string
|
|
47
|
+
"""
|
|
48
|
+
if len(tickers) * len(symbols) > cls.MAXIMUM_ITEMS_PER_BUNDLE:
|
|
49
|
+
raise ValueError(f"The maximum number of items for a bundle is {cls.MAXIMUM_ITEMS_PER_BUNDLE}")
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
len(tickers) * len(symbols) <= cls.MAXIMUM_ITEMS_PER_REQUEST
|
|
53
|
+
and len(tickers) <= cls.MAXIMUM_INSTRUMENTS_PER_REQUEST
|
|
54
|
+
):
|
|
55
|
+
yield tickers, symbols
|
|
56
|
+
else:
|
|
57
|
+
nb_tickers = min(
|
|
58
|
+
math.floor(cls.MAXIMUM_ITEMS_PER_REQUEST / len(symbols)),
|
|
59
|
+
len(tickers),
|
|
60
|
+
cls.MAXIMUM_INSTRUMENTS_PER_REQUEST,
|
|
61
|
+
)
|
|
62
|
+
yield tickers[0:nb_tickers], symbols
|
|
63
|
+
yield from cls._breakdown_requests(tickers[nb_tickers:], symbols)
|
|
64
|
+
|
|
65
|
+
def get_last_fx_rate(self, base_currency_key: str, target_currency_key: str):
|
|
66
|
+
last_currency_rate = CurrencyFXRates.objects.latest("date").date
|
|
67
|
+
try:
|
|
68
|
+
base_currency = Currency.objects.get(key=base_currency_key)
|
|
69
|
+
target_currency = Currency.objects.get(key=target_currency_key)
|
|
70
|
+
return float(base_currency.convert(last_currency_rate, target_currency, exact_lookup=True))
|
|
71
|
+
except Currency.DoesNotExist:
|
|
72
|
+
return 1.0
|
|
73
|
+
|
|
74
|
+
def _process_raw_requests(self, tickers: list[str], fields: list[str], **extra_client_kwargs) -> pd.DataFrame:
|
|
75
|
+
"""
|
|
76
|
+
Utility function to fetch data in bulk, aggregate and clean the result
|
|
77
|
+
Args:
|
|
78
|
+
tickers: Instruments tickers
|
|
79
|
+
fields: Symbols
|
|
80
|
+
**extra_client_kwargs: Extra keyword arguments to be passed down to the client (e.g. frequency)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
An aggregated and cleaned dataframe with [Dates, Instrument] as index for timeserie and [Instrument] As index for static data
|
|
84
|
+
"""
|
|
85
|
+
requests_data = list(self._breakdown_requests(tickers, fields))
|
|
86
|
+
reqs = []
|
|
87
|
+
if len(requests_data) > self.MAXIMUM_REQUESTS_PER_BUNDLE:
|
|
88
|
+
raise ValueError(f"number of request exceed {self.MAXIMUM_REQUESTS_PER_BUNDLE}")
|
|
89
|
+
# Construct the requests bundle
|
|
90
|
+
for request_tickers, request_symbols in requests_data:
|
|
91
|
+
# Convert a list of string into a valid string
|
|
92
|
+
converted_ticker = ",".join(request_tickers)
|
|
93
|
+
if "start" in extra_client_kwargs or "end" in extra_client_kwargs:
|
|
94
|
+
reqs.append(
|
|
95
|
+
self.connection.post_user_request(tickers=converted_ticker, fields=fields, **extra_client_kwargs)
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
reqs.append(self.connection.post_user_request(tickers=converted_ticker, fields=fields, kind=0))
|
|
99
|
+
# concat the bundle results
|
|
100
|
+
res = self.connection.get_bundle_data(bundleRequest=reqs)
|
|
101
|
+
|
|
102
|
+
df = pd.DataFrame()
|
|
103
|
+
if res:
|
|
104
|
+
if "start" in extra_client_kwargs or "end" in extra_client_kwargs:
|
|
105
|
+
res = list(filter(lambda subdf: subdf.index.name == "Dates", res))
|
|
106
|
+
if res:
|
|
107
|
+
df = pd.concat(res)
|
|
108
|
+
if df.index.name == "Dates":
|
|
109
|
+
df = (
|
|
110
|
+
pd.melt(df, ignore_index=False)
|
|
111
|
+
.reset_index()
|
|
112
|
+
.rename(columns={"value": "Value", "Field": "Datatype"})
|
|
113
|
+
)
|
|
114
|
+
df.Value = df.Value.apply(lambda x: None if str(x).startswith("$$ER") else x)
|
|
115
|
+
if not df.empty:
|
|
116
|
+
df = df[df.Value.notnull()]
|
|
117
|
+
df = df.replace({"NA": None})
|
|
118
|
+
if "Dates" in df.columns:
|
|
119
|
+
df.Dates = pd.to_datetime(df.Dates, utc=True)
|
|
120
|
+
indexes = ["Instrument", "Dates"]
|
|
121
|
+
else:
|
|
122
|
+
indexes = ["Instrument"]
|
|
123
|
+
df = pd.pivot_table(
|
|
124
|
+
df, values="Value", index=indexes, columns="Datatype", aggfunc="first", dropna=False
|
|
125
|
+
)
|
|
126
|
+
return df
|
|
127
|
+
|
|
128
|
+
def _normalize_df_units(
|
|
129
|
+
self,
|
|
130
|
+
df: pd.DataFrame,
|
|
131
|
+
ibes_non_per_share_fields: list[str] | None = None,
|
|
132
|
+
ibes_currency_based_fields: list[str] | None = None,
|
|
133
|
+
ibes_fields: list[str] | None = None,
|
|
134
|
+
**extra_client_kwargs,
|
|
135
|
+
) -> pd.DataFrame:
|
|
136
|
+
"""
|
|
137
|
+
Datastream use arbitrary units for some symbols where the denominator needs to be fetch in two steps:
|
|
138
|
+
* Using FIELD#U to get the datastream unit
|
|
139
|
+
* if ibes_non_per_share_fields is provided, fetch for the static symbol IBUNIT and normalized the given columns with the result times a constant (100000)
|
|
140
|
+
* if ibes_currency_based_fields is provided, fetch for the static symbol IBCUR and normalized the given columns latest found currency rate.
|
|
141
|
+
This function uses the index named "Instrument" as ticker fields and the columns as symbols. Start, End or Frequency needs to be passed down using the extra_client_kwargs argument
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
df: The dataframe to be normalized
|
|
145
|
+
ibes_non_per_share_fields: Columns that need ibunit normalization. Defaults to None
|
|
146
|
+
ibes_currency_based_fields: Columns that need IBES Currency conversion from static field IBCUR
|
|
147
|
+
**extra_client_kwargs: Extra keywords arguments to be passed down to the client (e.g. frequency)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A normalized dataframe (e.g. volume are in shares and not a million of shares)
|
|
151
|
+
"""
|
|
152
|
+
if not ibes_fields:
|
|
153
|
+
ibes_fields = []
|
|
154
|
+
df = df.copy()
|
|
155
|
+
fields = df.columns.unique().tolist()
|
|
156
|
+
tickers = df.index.get_level_values("Instrument").unique().tolist()
|
|
157
|
+
dfu = self._process_raw_requests(tickers, list(map(lambda x: x + "#U", fields)), **extra_client_kwargs).fillna(
|
|
158
|
+
1
|
|
159
|
+
)
|
|
160
|
+
dfu = dfu[dfu.index.isin(df.index)]
|
|
161
|
+
if not dfu.empty:
|
|
162
|
+
dfu = dfu.rename(columns=lambda x: x.replace("#U", "")).fillna(1)
|
|
163
|
+
dfu = dfu[dfu.columns.intersection(df.columns)]
|
|
164
|
+
dff = df[dfu.columns].multiply(dfu, fill_value=1)
|
|
165
|
+
|
|
166
|
+
# We do this to ensure that not provided data (nan) are not set with the multiplication with the U matrix
|
|
167
|
+
dff[df.isnull()] = df[df.isnull()]
|
|
168
|
+
dff[df.columns.difference(dff.columns)] = df[
|
|
169
|
+
df.columns.difference(dff.columns)
|
|
170
|
+
] # We ensure that the inital colums from df are appended to the new dataframe if missing from dfu. (happens when the colum has non number values)
|
|
171
|
+
df = dff
|
|
172
|
+
|
|
173
|
+
# If the symbol is computed directly from refinitiv, we need to normalize it as well given the formula (define as the field name)
|
|
174
|
+
for mav_field in list(filter(lambda x: "MAV#" in x, fields)):
|
|
175
|
+
re_matches = re.findall(r"X\(([^\)]+)\)", mav_field)
|
|
176
|
+
if len(re_matches) > 0 and re_matches[0] in dfu.columns:
|
|
177
|
+
df.loc[:, mav_field] = df.loc[:, mav_field] * dfu.loc[:, re_matches[0]]
|
|
178
|
+
|
|
179
|
+
if ibes_non_per_share_fields:
|
|
180
|
+
df_ibunit = self.get_static_df(tickers, ["IBUNIT"]).fillna(1)
|
|
181
|
+
if not df_ibunit.empty:
|
|
182
|
+
df_ibunit = df_ibunit.set_index("Instrument")["IBUNIT"]
|
|
183
|
+
df[df.columns.intersection(ibes_non_per_share_fields)] = (
|
|
184
|
+
df[df.columns.intersection(ibes_non_per_share_fields)].multiply(
|
|
185
|
+
df_ibunit, axis=0, level="Instrument"
|
|
186
|
+
)
|
|
187
|
+
* self.IBUNIT_DEFAULT_UNIT
|
|
188
|
+
)
|
|
189
|
+
ibes_fields = list(set(ibes_fields + ibes_non_per_share_fields))
|
|
190
|
+
if ibes_currency_based_fields:
|
|
191
|
+
df_ibcur = self.get_static_df(tickers, ["IBCUR", "ISOCUR"]).replace(
|
|
192
|
+
"BPN", "GBX"
|
|
193
|
+
) # BPN == GBX but our db only support the latter
|
|
194
|
+
if not df_ibcur.empty:
|
|
195
|
+
df_ibcur["rate"] = 1
|
|
196
|
+
different_curr_idx = df_ibcur["IBCUR"] != df_ibcur["ISOCUR"]
|
|
197
|
+
df_ibcur.loc[different_curr_idx, "rate"] = df_ibcur.loc[different_curr_idx].apply(
|
|
198
|
+
lambda x: self.get_last_fx_rate(x["IBCUR"], x["ISOCUR"]), axis=1
|
|
199
|
+
)
|
|
200
|
+
df[df.columns.intersection(ibes_currency_based_fields)] = df[
|
|
201
|
+
df.columns.intersection(ibes_currency_based_fields)
|
|
202
|
+
].multiply(df_ibcur.set_index("Instrument")["rate"], axis=0, level="Instrument")
|
|
203
|
+
ibes_fields = list(set(ibes_fields + ibes_currency_based_fields))
|
|
204
|
+
if ibes_fields:
|
|
205
|
+
df[df.columns.intersection(ibes_fields)] = (
|
|
206
|
+
df[df.columns.intersection(ibes_fields)] * self.IBUNIT_DEFAULT_UNIT
|
|
207
|
+
)
|
|
208
|
+
return df
|
|
209
|
+
|
|
210
|
+
def get_static_df(self, tickers: List[str], fields: List[str], **kwargs) -> pd.DataFrame:
|
|
211
|
+
"""
|
|
212
|
+
Public function to returns a dataframe for static symbols
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
tickers: Ticker to fetch
|
|
216
|
+
fields: Static symbols to fetch
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A valid dataframe result
|
|
220
|
+
"""
|
|
221
|
+
# Breakdown tickers and fields into a valid datastream parameters subsets
|
|
222
|
+
return self._process_raw_requests(tickers, fields).reset_index()
|
|
223
|
+
|
|
224
|
+
def get_timeserie_df(
|
|
225
|
+
self,
|
|
226
|
+
tickers: List[str],
|
|
227
|
+
fields: List[str],
|
|
228
|
+
ibes_non_per_share_fields: Optional[list[str]] = None,
|
|
229
|
+
ibes_currency_based_fields: Optional[list[str]] = None,
|
|
230
|
+
ibes_fields: list[str] | None = None,
|
|
231
|
+
**extra_client_kwargs,
|
|
232
|
+
) -> pd.DataFrame:
|
|
233
|
+
"""
|
|
234
|
+
Public function to get timeserie type data
|
|
235
|
+
Args:
|
|
236
|
+
tickers: Instrument tickers
|
|
237
|
+
fields: Symbols to fetch
|
|
238
|
+
ibes_non_per_share_fields: Columns that need ibunit normalization. Defaults to None
|
|
239
|
+
ibes_currency_based_fields: Columns that need IBES Currency conversion from static field IBCUR
|
|
240
|
+
**extra_client_kwargs: Extra keywords arguments to be passed down to the client (e.g. frequency)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
The result as a dataframe
|
|
244
|
+
"""
|
|
245
|
+
if (start := extra_client_kwargs.get("start", None)) and isinstance(start, date):
|
|
246
|
+
extra_client_kwargs["start"] = start.strftime("%Y-%m-%d")
|
|
247
|
+
if (end := extra_client_kwargs.get("end", None)) and isinstance(end, date):
|
|
248
|
+
extra_client_kwargs["end"] = end.strftime("%Y-%m-%d")
|
|
249
|
+
|
|
250
|
+
final_df_list = []
|
|
251
|
+
# Sometime, we get too many fields and therefore, we need to split them based on the maximum number of symbols allowed per request
|
|
252
|
+
for splited_fields in np.array_split(fields, math.ceil((len(fields) / self.MAXIMUM_DATATYPES_PER_REQUEST))):
|
|
253
|
+
# Breakdown tickers and fields into a valid datastream parameters subsets
|
|
254
|
+
df = self._process_raw_requests(tickers, list(splited_fields), **extra_client_kwargs)
|
|
255
|
+
if not df.empty:
|
|
256
|
+
df = self._normalize_df_units(
|
|
257
|
+
df,
|
|
258
|
+
ibes_non_per_share_fields=ibes_non_per_share_fields,
|
|
259
|
+
ibes_currency_based_fields=ibes_currency_based_fields,
|
|
260
|
+
ibes_fields=ibes_fields,
|
|
261
|
+
**extra_client_kwargs,
|
|
262
|
+
)
|
|
263
|
+
df = (
|
|
264
|
+
df.reset_index()
|
|
265
|
+
.rename_axis(None)
|
|
266
|
+
.replace([np.inf, -np.inf, np.nan], None)
|
|
267
|
+
.dropna(how="all", subset=df.columns.intersection(splited_fields))
|
|
268
|
+
)
|
|
269
|
+
if not df.empty:
|
|
270
|
+
if "Dates" in df.columns:
|
|
271
|
+
df = df.set_index(["Instrument", "Dates"])
|
|
272
|
+
else: # otherwise it's not a timeseries and we set index only on instrument
|
|
273
|
+
df = df.set_index(["Instrument"])
|
|
274
|
+
final_df_list.append(df)
|
|
275
|
+
if final_df_list:
|
|
276
|
+
return pd.concat(final_df_list, axis=1).reset_index()
|
|
277
|
+
return pd.DataFrame()
|
|
278
|
+
|
|
279
|
+
def raw_fetch(
|
|
280
|
+
self, tickers: List[str], fields: List[str], start: Optional[date] = None, end: Optional[date] = None
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
Utility function to expose the client directly without any modification
|
|
284
|
+
"""
|
|
285
|
+
return self.connection.get_data(tickers=tickers, fields=fields, start=start, end=end)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
|
|
5
|
+
from wbcore.contrib.dataloader.dataloaders import Dataloader
|
|
6
|
+
from wbfdm.dataloaders.protocols import MarketDataProtocol
|
|
7
|
+
from wbfdm.dataloaders.types import MarketDataDict
|
|
8
|
+
from wbfdm.enums import MarketData
|
|
9
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
10
|
+
|
|
11
|
+
MarketDataMap = {
|
|
12
|
+
"SHARES_OUTSTANDING": "internal_outstanding_shares",
|
|
13
|
+
"OPEN": "net_value",
|
|
14
|
+
"CLOSE": "net_value",
|
|
15
|
+
"HIGH": "net_value",
|
|
16
|
+
"LOW": "net_value",
|
|
17
|
+
"BID": "net_value",
|
|
18
|
+
"ASK": "net_value",
|
|
19
|
+
"VOLUME": "internal_volume",
|
|
20
|
+
"MARKET_CAPITALIZATION": "market_capitalization",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
DEFAULT_VALUES = [MarketData[name] for name in MarketDataMap.keys()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cast_decimal_to_float(value: float | Decimal) -> float:
|
|
27
|
+
if isinstance(value, Decimal):
|
|
28
|
+
value = float(value)
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MarketDataDataloader(MarketDataProtocol, Dataloader):
|
|
33
|
+
def market_data(
|
|
34
|
+
self,
|
|
35
|
+
values: list[MarketData] | None = [MarketData.CLOSE],
|
|
36
|
+
from_date: date | None = None,
|
|
37
|
+
to_date: date | None = None,
|
|
38
|
+
exact_date: date | None = None,
|
|
39
|
+
calculated: bool | None = None,
|
|
40
|
+
**kwargs,
|
|
41
|
+
) -> Iterator[MarketDataDict]:
|
|
42
|
+
"""Get prices for instruments.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
values (list[MarketData]): List of values to include in the results.
|
|
46
|
+
from_date (date | None): The starting date for filtering prices. Defaults to None.
|
|
47
|
+
to_date (date | None): The ending date for filtering prices. Defaults to None.
|
|
48
|
+
frequency (Frequency): The frequency of the requested data
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Iterator[MarketDataDict]: An iterator of dictionaries conforming to the DailyValuationDict.
|
|
52
|
+
"""
|
|
53
|
+
prices = InstrumentPrice.objects.filter(instrument__in=self.entities).annotate_market_data() # type: ignore
|
|
54
|
+
if calculated is not None:
|
|
55
|
+
prices = prices.filter(calculated=calculated)
|
|
56
|
+
else:
|
|
57
|
+
prices = prices.filter_only_valid_prices()
|
|
58
|
+
|
|
59
|
+
prices = prices.order_by("date")
|
|
60
|
+
if not values:
|
|
61
|
+
values = DEFAULT_VALUES
|
|
62
|
+
values_map = {value.name: MarketDataMap[value.name] for value in values if value.name in MarketDataMap}
|
|
63
|
+
if exact_date:
|
|
64
|
+
prices = prices.filter(date=exact_date)
|
|
65
|
+
else:
|
|
66
|
+
if from_date:
|
|
67
|
+
prices = prices.filter(date__gte=from_date)
|
|
68
|
+
if to_date:
|
|
69
|
+
prices = prices.filter(date__lte=to_date)
|
|
70
|
+
for row in prices.filter_only_valid_prices().values(
|
|
71
|
+
"date",
|
|
72
|
+
"instrument",
|
|
73
|
+
"calculated",
|
|
74
|
+
*set(values_map.values()),
|
|
75
|
+
):
|
|
76
|
+
external_id = row.pop("instrument")
|
|
77
|
+
val_date = row.pop("date")
|
|
78
|
+
if row:
|
|
79
|
+
yield MarketDataDict(
|
|
80
|
+
id=f"{external_id}_{val_date}",
|
|
81
|
+
valuation_date=val_date,
|
|
82
|
+
instrument_id=external_id,
|
|
83
|
+
external_id=external_id,
|
|
84
|
+
source="wbfdm",
|
|
85
|
+
calculated=row["calculated"],
|
|
86
|
+
**{MarketData[k].value: _cast_decimal_to_float(row[v]) for k, v in values_map.items()},
|
|
87
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from wbfdm.admin.instruments import InstrumentModelAdmin as BaseInstrumentModelAdmin
|
|
3
|
+
from wbfdm.models.instruments import Instrument
|
|
4
|
+
|
|
5
|
+
from .metrics import InstrumentMetricGenericInline
|
|
6
|
+
|
|
7
|
+
admin.site.unregister(Instrument)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@admin.register(Instrument)
|
|
11
|
+
class InstrumentModelAdmin(BaseInstrumentModelAdmin):
|
|
12
|
+
inlines = (*BaseInstrumentModelAdmin.inlines, InstrumentMetricGenericInline)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.contenttypes.admin import GenericTabularInline
|
|
3
|
+
|
|
4
|
+
from ..models import InstrumentMetric
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Register your models here.
|
|
8
|
+
class InstrumentMetricGenericInline(GenericTabularInline):
|
|
9
|
+
model = InstrumentMetric
|
|
10
|
+
|
|
11
|
+
ordering = ["-date", "key"]
|
|
12
|
+
ct_field = "basket_content_type"
|
|
13
|
+
ct_fk_field = "basket_id"
|
|
14
|
+
raw_id_fields = ["basket_content_type", "instrument", "parent_metric"]
|
|
15
|
+
|
|
16
|
+
fields = [
|
|
17
|
+
"date",
|
|
18
|
+
"key",
|
|
19
|
+
"metrics",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def get_queryset(self, request):
|
|
23
|
+
return super().get_queryset(request).select_related("basket_content_type", "instrument")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@admin.register(InstrumentMetric)
|
|
27
|
+
class InstrumentMetricModelAdmin(admin.ModelAdmin):
|
|
28
|
+
list_filter = ("key",)
|
|
29
|
+
search_fields = ("instrument__computed_str",)
|
|
30
|
+
list_display = (
|
|
31
|
+
"basket_content_type",
|
|
32
|
+
"basket_id",
|
|
33
|
+
"instrument",
|
|
34
|
+
"date",
|
|
35
|
+
"key",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
autocomplete_fields = [
|
|
39
|
+
"basket_content_type",
|
|
40
|
+
"instrument",
|
|
41
|
+
]
|
|
42
|
+
ordering = ("-date", "-key")
|
|
43
|
+
raw_id_fields = ["basket_content_type", "instrument", "parent_metric"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.module_loading import autodiscover_modules
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MetricConfig(AppConfig):
|
|
6
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
7
|
+
name = "wbfdm.contrib.metric"
|
|
8
|
+
|
|
9
|
+
def ready(self) -> None:
|
|
10
|
+
autodiscover_modules("metric.backends")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Generator, Generic, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.db.models import F, Model, QuerySet, Value
|
|
9
|
+
from rest_framework.serializers import Field
|
|
10
|
+
from wbcore import serializers as wb_serializers
|
|
11
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
12
|
+
from wbfdm.models import Instrument, InstrumentPrice
|
|
13
|
+
|
|
14
|
+
from ..dto import Metric, MetricField, MetricKey
|
|
15
|
+
from ..exceptions import MetricInvalidParameterException
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T", bound=Model)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbstractBackend(Generic[T]):
|
|
21
|
+
BASKET_MODEL_CLASS: Type[Model]
|
|
22
|
+
keys: list[MetricKey]
|
|
23
|
+
|
|
24
|
+
def __init__(self, val_date: date | None):
|
|
25
|
+
self.val_date = val_date
|
|
26
|
+
if not self.BASKET_MODEL_CLASS:
|
|
27
|
+
raise ValueError("A class implementing AbstractBackend needs to define a BASKET_MODEL_CLASS")
|
|
28
|
+
self.content_type = ContentType.objects.get_for_model(self.BASKET_MODEL_CLASS)
|
|
29
|
+
|
|
30
|
+
def compute_metrics(self, basket: Any) -> Generator[Metric, None, None]:
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
def get_queryset(self) -> QuerySet:
|
|
34
|
+
return self.BASKET_MODEL_CLASS.objects.all()
|
|
35
|
+
|
|
36
|
+
def get_serializer_field_attr(self, metric_field: MetricField) -> dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Returns all the serializer attributes for that metric
|
|
39
|
+
|
|
40
|
+
We expect the implementing backends to override this method to define custom logics
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
"decorators": metric_field.decorators,
|
|
44
|
+
"help_text": metric_field.help_text,
|
|
45
|
+
**metric_field.serializer_kwargs,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def get_serializer_fields(
|
|
49
|
+
self, with_prefixed_key: bool = False, metric_key: MetricKey | None = None
|
|
50
|
+
) -> dict[str, Field]:
|
|
51
|
+
if metric_key is None:
|
|
52
|
+
metric_keys = self.keys
|
|
53
|
+
else:
|
|
54
|
+
metric_keys = [metric_key]
|
|
55
|
+
fields = {}
|
|
56
|
+
for metric_key in metric_keys:
|
|
57
|
+
fields.update(
|
|
58
|
+
{
|
|
59
|
+
field_key: wb_serializers.FloatField(
|
|
60
|
+
label=field_title,
|
|
61
|
+
read_only=True,
|
|
62
|
+
**self.get_serializer_field_attr(metric_key.subfields_map[field_key]),
|
|
63
|
+
)
|
|
64
|
+
for field_key, field_title in metric_key.get_fields(with_prefixed_key=with_prefixed_key)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
for extra_subfield in metric_key.extra_subfields:
|
|
68
|
+
fields[
|
|
69
|
+
f"{metric_key.key}_{extra_subfield.key}"
|
|
70
|
+
] = wb_serializers.ModelSerializer.serializer_field_mapping[extra_subfield.field_type](
|
|
71
|
+
read_only=True, label=extra_subfield.label, **self.get_serializer_field_attr(extra_subfield)
|
|
72
|
+
)
|
|
73
|
+
return fields
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InstrumentMetricBaseBackend(AbstractBackend[Instrument]):
|
|
77
|
+
BASKET_MODEL_CLASS = Instrument
|
|
78
|
+
TARGET_CURRENCY_KEY: str | None = None
|
|
79
|
+
|
|
80
|
+
def get_queryset(self) -> QuerySet[Instrument]:
|
|
81
|
+
return super().get_queryset().filter(is_investable_universe=True)
|
|
82
|
+
|
|
83
|
+
def _get_valid_date(self, instrument: Instrument) -> date:
|
|
84
|
+
if self.val_date is None and instrument.last_price_date:
|
|
85
|
+
return instrument.last_price_date
|
|
86
|
+
elif self.val_date:
|
|
87
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
88
|
+
return instrument.prices.filter(date__lte=self.val_date).latest("date").date
|
|
89
|
+
else:
|
|
90
|
+
raise MetricInvalidParameterException()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseDataloader:
|
|
94
|
+
METRIC_KEY: str
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
basket,
|
|
99
|
+
val_date: date | None = None,
|
|
100
|
+
min_date: date | None = None,
|
|
101
|
+
target_currency_key: str | None = None,
|
|
102
|
+
use_cached_metrics_key: str | None = None,
|
|
103
|
+
basket_objects: QuerySet | None = None,
|
|
104
|
+
):
|
|
105
|
+
self.val_date = val_date
|
|
106
|
+
self.min_date = min_date
|
|
107
|
+
self.basket = basket
|
|
108
|
+
self.use_cached_metrics_key = use_cached_metrics_key
|
|
109
|
+
self.target_currency_key = target_currency_key
|
|
110
|
+
self.basket_objects = basket_objects
|
|
111
|
+
if self.basket_objects is None:
|
|
112
|
+
self.basket_objects = self._get_basket_basket_objects()
|
|
113
|
+
self.basket_object_ids = list(map(lambda x: x.id, self.basket_objects))
|
|
114
|
+
self.aggregate_callback = "mean"
|
|
115
|
+
if self.target_currency_key == "USD":
|
|
116
|
+
self.fx_rate_expression = F("currency_fx_rate_to_usd__value")
|
|
117
|
+
elif self.target_currency_key:
|
|
118
|
+
self.fx_rate_expression = CurrencyFXRates.get_fx_rates_subquery_for_two_currencies(
|
|
119
|
+
"date", start_currency="instrument__currency", target_currency=self.target_currency_key
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
self.fx_rate_expression = Value(Decimal(1.0))
|
|
123
|
+
|
|
124
|
+
def _get_basket_basket_objects(self) -> QuerySet:
|
|
125
|
+
"""
|
|
126
|
+
Return an iterable of instrument contains within the basket. Expected to be override for custom logic
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
An iterable of instrument. Default to the basket itself
|
|
130
|
+
"""
|
|
131
|
+
return Instrument.objects.filter(id=self.basket.id)
|
|
132
|
+
|
|
133
|
+
def _compute(self):
|
|
134
|
+
raise NotImplementedError()
|
|
135
|
+
|
|
136
|
+
def compute(self):
|
|
137
|
+
if self.basket_object_ids:
|
|
138
|
+
if self.use_cached_metrics_key:
|
|
139
|
+
from wbfdm.contrib.metric.models import InstrumentMetric
|
|
140
|
+
|
|
141
|
+
df = pd.json_normalize(
|
|
142
|
+
InstrumentMetric.objects.filter(
|
|
143
|
+
basket_content_type=ContentType.objects.get_for_model(self.basket_objects.model),
|
|
144
|
+
basket_id__in=self.basket_object_ids,
|
|
145
|
+
key=self.use_cached_metrics_key,
|
|
146
|
+
date__isnull=True,
|
|
147
|
+
).values_list("metrics", flat=True)
|
|
148
|
+
)
|
|
149
|
+
if not df.empty:
|
|
150
|
+
if (
|
|
151
|
+
"date" in df.columns
|
|
152
|
+
): # it can happen that the cached metric don't contain a date key value, so we ffill it in case
|
|
153
|
+
df["date"] = df["date"].ffill()
|
|
154
|
+
if isinstance(self.aggregate_callback, dict):
|
|
155
|
+
df = df.agg({k: v for k, v in self.aggregate_callback.items() if k in df.columns})
|
|
156
|
+
else:
|
|
157
|
+
df = df.agg(self.aggregate_callback)
|
|
158
|
+
return df.dropna().to_dict()
|
|
159
|
+
return self._compute()
|