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,152 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
7
|
+
from faker import Faker
|
|
8
|
+
from pandas.tseries.offsets import BDay
|
|
9
|
+
from wbfdm.models import Instrument, RelatedInstrumentThroughModel
|
|
10
|
+
|
|
11
|
+
fake = Faker()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.django_db
|
|
15
|
+
class TestInstrumentPerformanceMetricBackend:
|
|
16
|
+
def test_compute_metrics(self, weekday, instrument, instrument_price_factory):
|
|
17
|
+
from wbfdm.contrib.metric.backends.performances import ( # we import locally to avoid database access pytest error
|
|
18
|
+
InstrumentPerformanceMetricBackend,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
backend = InstrumentPerformanceMetricBackend(weekday)
|
|
22
|
+
|
|
23
|
+
price_today = instrument_price_factory.create(instrument=instrument, calculated=False, date=weekday)
|
|
24
|
+
price_yesterday = instrument_price_factory.create(
|
|
25
|
+
instrument=instrument, calculated=False, date=weekday - BDay(1)
|
|
26
|
+
)
|
|
27
|
+
price_last_week = instrument_price_factory.create(
|
|
28
|
+
instrument=instrument, calculated=False, date=(weekday - timedelta(days=7)) - BDay(0)
|
|
29
|
+
)
|
|
30
|
+
price_last_month = instrument_price_factory.create(
|
|
31
|
+
instrument=instrument, calculated=False, date=(weekday - timedelta(days=30)) - BDay(1)
|
|
32
|
+
)
|
|
33
|
+
price_last_quarter = instrument_price_factory.create(
|
|
34
|
+
instrument=instrument, calculated=False, date=(weekday - timedelta(days=120)) - BDay(1)
|
|
35
|
+
)
|
|
36
|
+
price_last_year = instrument_price_factory.create(
|
|
37
|
+
instrument=instrument, calculated=False, date=(weekday - timedelta(days=365)) - BDay(1)
|
|
38
|
+
)
|
|
39
|
+
price_end_last_year = instrument_price_factory.create(
|
|
40
|
+
instrument=instrument, calculated=False, date=weekday - pd.tseries.offsets.BYearEnd(1)
|
|
41
|
+
)
|
|
42
|
+
price_inception = instrument_price_factory.create(
|
|
43
|
+
instrument=instrument, calculated=False, date=price_last_year.date - BDay(1)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
instrument_price_factory.create(
|
|
47
|
+
instrument=instrument, calculated=True, date=price_inception.date - BDay(1)
|
|
48
|
+
) # add noise
|
|
49
|
+
instrument.inception_date = price_inception.date.date()
|
|
50
|
+
instrument.save()
|
|
51
|
+
instrument.update_last_valuation_date()
|
|
52
|
+
base_metric = next(backend.compute_metrics(instrument))
|
|
53
|
+
|
|
54
|
+
# Check base metric metadata
|
|
55
|
+
assert base_metric.date is None
|
|
56
|
+
assert base_metric.basket_id == instrument.id
|
|
57
|
+
assert base_metric.basket_content_type_id == ContentType.objects.get_for_model(Instrument).id
|
|
58
|
+
assert base_metric.key == "performance"
|
|
59
|
+
assert base_metric.instrument_id is None
|
|
60
|
+
|
|
61
|
+
assert base_metric.metrics["is_estimated"] is False
|
|
62
|
+
assert base_metric.metrics["daily"] == pytest.approx(
|
|
63
|
+
float(price_today.net_value / price_yesterday.net_value - Decimal(1)), abs=1e-5
|
|
64
|
+
)
|
|
65
|
+
assert base_metric.metrics["weekly"] == pytest.approx(
|
|
66
|
+
float(price_today.net_value / price_last_week.net_value - Decimal(1)), abs=1e-5
|
|
67
|
+
)
|
|
68
|
+
assert base_metric.metrics["monthly"] == pytest.approx(
|
|
69
|
+
float(price_today.net_value / price_last_month.net_value - Decimal(1)), abs=1e-5
|
|
70
|
+
)
|
|
71
|
+
assert base_metric.metrics["quarterly"] == pytest.approx(
|
|
72
|
+
float(price_today.net_value / price_last_quarter.net_value - Decimal(1)), abs=1e-5
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert base_metric.metrics["yearly"] == pytest.approx(
|
|
76
|
+
float(price_today.net_value / price_last_year.net_value - Decimal(1)), abs=1e-5
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert base_metric.metrics["year_to_date"] == pytest.approx(
|
|
80
|
+
float(price_today.net_value / price_end_last_year.net_value - Decimal(1)), abs=1e-5
|
|
81
|
+
)
|
|
82
|
+
assert base_metric.metrics["inception"] == pytest.approx(
|
|
83
|
+
float(price_today.net_value / price_inception.net_value - Decimal(1)), abs=1e-5
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_compute_metrics_estimated(self, weekday, instrument, instrument_price_factory):
|
|
87
|
+
from wbfdm.contrib.metric.backends.performances import ( # we import locally to avoid database access pytest error
|
|
88
|
+
InstrumentPerformanceMetricBackend,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
backend = InstrumentPerformanceMetricBackend(weekday)
|
|
92
|
+
|
|
93
|
+
price_today = instrument_price_factory.create(instrument=instrument, calculated=True, date=weekday)
|
|
94
|
+
price_yesterday = instrument_price_factory.create(
|
|
95
|
+
instrument=instrument, calculated=False, date=weekday - BDay(1)
|
|
96
|
+
)
|
|
97
|
+
instrument.inception_date = price_yesterday.date.date()
|
|
98
|
+
instrument.save()
|
|
99
|
+
instrument.update_last_valuation_date()
|
|
100
|
+
base_metric = next(backend.compute_metrics(instrument))
|
|
101
|
+
|
|
102
|
+
assert base_metric.metrics["is_estimated"] is True
|
|
103
|
+
assert base_metric.metrics["daily"] == pytest.approx(
|
|
104
|
+
float(price_today.net_value / price_yesterday.net_value - Decimal(1)), abs=1e-5
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def test_compute_metrics_with_peers(self, weekday, instrument_factory, instrument_price_factory):
|
|
108
|
+
from wbfdm.contrib.metric.backends.performances import ( # we import locally to avoid database access pytest error
|
|
109
|
+
InstrumentPerformanceMetricBackend,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
instrument = instrument_factory.create()
|
|
113
|
+
peer_1 = instrument_factory.create()
|
|
114
|
+
RelatedInstrumentThroughModel.objects.create(
|
|
115
|
+
related_type="PEER", instrument=instrument, related_instrument=peer_1
|
|
116
|
+
)
|
|
117
|
+
peer_2 = instrument_factory.create()
|
|
118
|
+
RelatedInstrumentThroughModel.objects.create(
|
|
119
|
+
related_type="PEER", instrument=instrument, related_instrument=peer_2
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
backend = InstrumentPerformanceMetricBackend(weekday)
|
|
123
|
+
|
|
124
|
+
price_today = instrument_price_factory.create(instrument=instrument, calculated=False, date=weekday)
|
|
125
|
+
price_yesterday = instrument_price_factory.create(
|
|
126
|
+
instrument=instrument, calculated=False, date=weekday - BDay(1)
|
|
127
|
+
)
|
|
128
|
+
instrument.inception_date = price_yesterday.date.date()
|
|
129
|
+
instrument.save()
|
|
130
|
+
peer_1_price_today = instrument_price_factory.create(instrument=peer_1, calculated=False, date=weekday)
|
|
131
|
+
peer_1_price_yesterday = instrument_price_factory.create(
|
|
132
|
+
instrument=peer_1, calculated=False, date=weekday - BDay(1)
|
|
133
|
+
)
|
|
134
|
+
peer_1.inception_date = peer_1_price_yesterday.date.date()
|
|
135
|
+
peer_1.save()
|
|
136
|
+
peer_2_price_today = instrument_price_factory.create(instrument=peer_2, calculated=False, date=weekday)
|
|
137
|
+
peer_2_price_yesterday = instrument_price_factory.create(
|
|
138
|
+
instrument=peer_2, calculated=False, date=weekday - BDay(1)
|
|
139
|
+
)
|
|
140
|
+
peer_2.inception_date = peer_2_price_yesterday.date.date()
|
|
141
|
+
peer_2.save()
|
|
142
|
+
instrument.update_last_valuation_date()
|
|
143
|
+
peer_1.update_last_valuation_date()
|
|
144
|
+
peer_2.update_last_valuation_date()
|
|
145
|
+
base_metric = next(backend.compute_metrics(instrument))
|
|
146
|
+
daily_instrument_perf = float(price_today.net_value / price_yesterday.net_value - Decimal(1))
|
|
147
|
+
assert base_metric.metrics["daily"] == pytest.approx(daily_instrument_perf, abs=1e-5)
|
|
148
|
+
daily_peer_1_perf = float(peer_1_price_today.net_value / peer_1_price_yesterday.net_value - Decimal(1))
|
|
149
|
+
daily_peer_2_perf = float(peer_2_price_today.net_value / peer_2_price_yesterday.net_value - Decimal(1))
|
|
150
|
+
avg_daily_peer_perf = (daily_peer_1_perf + daily_peer_2_perf) / 2
|
|
151
|
+
r = daily_instrument_perf - avg_daily_peer_perf
|
|
152
|
+
assert base_metric.metrics["peer_daily"] == pytest.approx(r, abs=0.00001)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from faker import Faker
|
|
6
|
+
from wbfdm.dataloaders.proxies import InstrumentDataloaderProxy
|
|
7
|
+
|
|
8
|
+
fake = Faker()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.django_db
|
|
12
|
+
class TestInstrumentFinancialStatisticsMetricBackend:
|
|
13
|
+
@patch.object(InstrumentDataloaderProxy, "financials")
|
|
14
|
+
def test_default_daily_statistics(self, mock_fct, weekday, instrument, instrument_price_factory):
|
|
15
|
+
from wbfdm.contrib.metric.backends.statistics import ( # we import locally to avoid database access pytest error
|
|
16
|
+
InstrumentFinancialStatisticsMetricBackend,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
backend = InstrumentFinancialStatisticsMetricBackend(weekday)
|
|
20
|
+
|
|
21
|
+
revenue_y_1 = fake.pyfloat()
|
|
22
|
+
revenue_y0 = fake.pyfloat()
|
|
23
|
+
revenue_y1 = fake.pyfloat()
|
|
24
|
+
market_capitalization = fake.pyfloat()
|
|
25
|
+
price = Decimal(150)
|
|
26
|
+
volume_50d = fake.pyfloat()
|
|
27
|
+
instrument_price_factory.create(
|
|
28
|
+
instrument=instrument,
|
|
29
|
+
calculated=False,
|
|
30
|
+
date=weekday,
|
|
31
|
+
net_value=price,
|
|
32
|
+
volume_50d=volume_50d,
|
|
33
|
+
market_capitalization=market_capitalization,
|
|
34
|
+
)
|
|
35
|
+
instrument.update_last_valuation_date()
|
|
36
|
+
mock_fct.return_value = [
|
|
37
|
+
{"year": weekday.year - 1, "value": revenue_y_1, "instrument_id": instrument.id},
|
|
38
|
+
{"year": weekday.year, "value": revenue_y0, "instrument_id": instrument.id},
|
|
39
|
+
{"year": weekday.year + 1, "value": revenue_y1, "instrument_id": instrument.id},
|
|
40
|
+
]
|
|
41
|
+
metrics = next(backend.compute_metrics(instrument)).metrics
|
|
42
|
+
|
|
43
|
+
assert metrics["revenue_y_1"] == revenue_y_1
|
|
44
|
+
assert metrics["revenue_y0"] == revenue_y0
|
|
45
|
+
assert metrics["revenue_y1"] == revenue_y1
|
|
46
|
+
assert metrics["market_capitalization"] == market_capitalization
|
|
47
|
+
assert metrics["price"] == price
|
|
48
|
+
assert metrics["volume_50d"] == volume_50d
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from wbcore.tests.conftest import * # type: ignore # isort:skip
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
from django.apps import apps
|
|
5
|
+
from django.db.models.signals import pre_migrate
|
|
6
|
+
from faker import Faker
|
|
7
|
+
from pandas.tseries.offsets import BDay
|
|
8
|
+
from pytest_factoryboy import register
|
|
9
|
+
from wbcore.contrib.authentication.factories import (
|
|
10
|
+
InternalUserFactory,
|
|
11
|
+
SuperUserFactory,
|
|
12
|
+
UserFactory,
|
|
13
|
+
)
|
|
14
|
+
from wbcore.contrib.currency.factories import CurrencyFactory, CurrencyFXRatesFactory
|
|
15
|
+
from wbcore.contrib.directory.factories.entries import (
|
|
16
|
+
CompanyFactory,
|
|
17
|
+
CompanyTypeFactory,
|
|
18
|
+
CustomerStatusFactory,
|
|
19
|
+
EntryFactory,
|
|
20
|
+
PersonFactory,
|
|
21
|
+
)
|
|
22
|
+
from wbcore.contrib.geography.factories import (
|
|
23
|
+
CityFactory,
|
|
24
|
+
ContinentFactory,
|
|
25
|
+
CountryFactory,
|
|
26
|
+
StateFactory,
|
|
27
|
+
)
|
|
28
|
+
from wbcore.contrib.geography.tests.signals import app_pre_migration
|
|
29
|
+
from wbcore.contrib.io.factories import (
|
|
30
|
+
CrontabScheduleFactory,
|
|
31
|
+
DataBackendFactory,
|
|
32
|
+
ImportSourceFactory,
|
|
33
|
+
ParserHandlerFactory,
|
|
34
|
+
ProviderFactory,
|
|
35
|
+
SourceFactory,
|
|
36
|
+
)
|
|
37
|
+
from wbfdm.factories import (
|
|
38
|
+
ClassificationFactory,
|
|
39
|
+
ClassificationGroupFactory,
|
|
40
|
+
ExchangeFactory,
|
|
41
|
+
InstrumentClassificationThroughModelFactory,
|
|
42
|
+
InstrumentFactory,
|
|
43
|
+
InstrumentFavoriteGroupFactory,
|
|
44
|
+
InstrumentListFactory,
|
|
45
|
+
InstrumentPriceFactory,
|
|
46
|
+
InstrumentTypeFactory,
|
|
47
|
+
ParentClassificationFactory,
|
|
48
|
+
RelatedInstrumentThroughModelFactory,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from ..factories import InstrumentMetricFactory
|
|
52
|
+
|
|
53
|
+
fake = Faker()
|
|
54
|
+
register(ImportSourceFactory)
|
|
55
|
+
register(DataBackendFactory)
|
|
56
|
+
register(ProviderFactory)
|
|
57
|
+
register(SourceFactory)
|
|
58
|
+
register(ParserHandlerFactory)
|
|
59
|
+
register(CrontabScheduleFactory)
|
|
60
|
+
|
|
61
|
+
register(ClassificationFactory)
|
|
62
|
+
register(InstrumentClassificationThroughModelFactory)
|
|
63
|
+
register(ParentClassificationFactory)
|
|
64
|
+
register(ClassificationGroupFactory)
|
|
65
|
+
register(ExchangeFactory)
|
|
66
|
+
register(InstrumentFactory)
|
|
67
|
+
register(InstrumentTypeFactory)
|
|
68
|
+
register(InstrumentPriceFactory)
|
|
69
|
+
register(InstrumentFavoriteGroupFactory)
|
|
70
|
+
register(RelatedInstrumentThroughModelFactory)
|
|
71
|
+
register(CurrencyFXRatesFactory)
|
|
72
|
+
register(InstrumentListFactory)
|
|
73
|
+
|
|
74
|
+
register(CurrencyFactory)
|
|
75
|
+
register(CityFactory)
|
|
76
|
+
register(StateFactory)
|
|
77
|
+
register(CountryFactory)
|
|
78
|
+
register(ContinentFactory)
|
|
79
|
+
|
|
80
|
+
register(CompanyFactory)
|
|
81
|
+
register(PersonFactory)
|
|
82
|
+
register(InternalUserFactory)
|
|
83
|
+
register(EntryFactory)
|
|
84
|
+
register(CustomerStatusFactory)
|
|
85
|
+
register(CompanyTypeFactory)
|
|
86
|
+
|
|
87
|
+
register(UserFactory)
|
|
88
|
+
register(SuperUserFactory, "superuser")
|
|
89
|
+
|
|
90
|
+
register(InstrumentMetricFactory)
|
|
91
|
+
|
|
92
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("metric"))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from faker import Faker
|
|
3
|
+
|
|
4
|
+
from ..backends.performances import InstrumentMetricBaseBackend
|
|
5
|
+
from ..dto import MetricField, MetricKey
|
|
6
|
+
|
|
7
|
+
fake = Faker()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.django_db
|
|
11
|
+
class TestMetricKey:
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def metric_key(self):
|
|
14
|
+
label = fake.word()
|
|
15
|
+
|
|
16
|
+
return MetricKey(
|
|
17
|
+
key=label.lower(),
|
|
18
|
+
label=label,
|
|
19
|
+
additional_prefixes=[fake.word()],
|
|
20
|
+
subfields=[
|
|
21
|
+
MetricField(
|
|
22
|
+
key=label.lower(),
|
|
23
|
+
label=label,
|
|
24
|
+
decorators=[{"position": "left", "value": "&", "type": "text"}],
|
|
25
|
+
serializer_kwargs={"percent": fake.boolean(), "precision": fake.pyint(min_value=1, max_value=4)},
|
|
26
|
+
)
|
|
27
|
+
for label in map(lambda x: fake.word(), range(3))
|
|
28
|
+
],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_get_fields(self, metric_key):
|
|
32
|
+
fields = list(metric_key.get_fields())
|
|
33
|
+
assert fields[0] == (
|
|
34
|
+
f"{metric_key.key}_{metric_key.subfields[0].key}",
|
|
35
|
+
f"{metric_key.label} {metric_key.subfields[0].label}",
|
|
36
|
+
)
|
|
37
|
+
assert fields[1] == (
|
|
38
|
+
f"{metric_key.key}_{metric_key.subfields[1].key}",
|
|
39
|
+
f"{metric_key.label} {metric_key.subfields[1].label}",
|
|
40
|
+
)
|
|
41
|
+
assert fields[2] == (
|
|
42
|
+
f"{metric_key.key}_{metric_key.subfields[2].key}",
|
|
43
|
+
f"{metric_key.label} {metric_key.subfields[2].label}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def test_get_fields_with_prefixed_key(self, metric_key):
|
|
47
|
+
fields = list(metric_key.get_fields(with_prefixed_key=True))
|
|
48
|
+
prefix = metric_key.additional_prefixes[0]
|
|
49
|
+
assert fields[1] == (
|
|
50
|
+
f"{metric_key.key}_{prefix}_{metric_key.subfields[0].key}",
|
|
51
|
+
f"{metric_key.label} {metric_key.subfields[0].label} ({prefix.title()})",
|
|
52
|
+
)
|
|
53
|
+
assert fields[3] == (
|
|
54
|
+
f"{metric_key.key}_{prefix}_{metric_key.subfields[1].key}",
|
|
55
|
+
f"{metric_key.label} {metric_key.subfields[1].label} ({prefix.title()})",
|
|
56
|
+
)
|
|
57
|
+
assert fields[5] == (
|
|
58
|
+
f"{metric_key.key}_{prefix}_{metric_key.subfields[2].key}",
|
|
59
|
+
f"{metric_key.label} {metric_key.subfields[2].label} ({prefix.title()})",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def test_fields_map(self, metric_key):
|
|
63
|
+
keys = set(map(lambda x: x[0], metric_key.get_fields(with_prefixed_key=True)))
|
|
64
|
+
assert keys == set(metric_key.subfields_filter_map.keys())
|
|
65
|
+
|
|
66
|
+
def test_get_serializer_field_attr(self, weekday, metric_key):
|
|
67
|
+
metric_field = metric_key.subfields[0]
|
|
68
|
+
backend = InstrumentMetricBaseBackend(weekday)
|
|
69
|
+
assert backend.get_serializer_field_attr(metric_field)["percent"] == metric_field.serializer_kwargs["percent"]
|
|
70
|
+
assert (
|
|
71
|
+
backend.get_serializer_field_attr(metric_field)["precision"] == metric_field.serializer_kwargs["precision"]
|
|
72
|
+
)
|
|
73
|
+
assert backend.get_serializer_field_attr(metric_field)["decorators"] == metric_field.decorators
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from wbfdm.models import Instrument
|
|
4
|
+
|
|
5
|
+
from ..dto import Metric
|
|
6
|
+
from ..models import InstrumentMetric
|
|
7
|
+
from ..registry import backend_registry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.django_db
|
|
11
|
+
class TestInstrumentMetric:
|
|
12
|
+
def test_save(self, instrument_metric):
|
|
13
|
+
assert instrument_metric.basket_repr == str(instrument_metric.basket)
|
|
14
|
+
|
|
15
|
+
def test_update_or_create_from_metric(self, weekday, instrument_factory):
|
|
16
|
+
i1 = instrument_factory.create()
|
|
17
|
+
i2 = instrument_factory.create()
|
|
18
|
+
i3 = instrument_factory.create()
|
|
19
|
+
key = "key"
|
|
20
|
+
basket_content_type_id = ContentType.objects.get_for_model(i1).id
|
|
21
|
+
level1_dto_metric = Metric(
|
|
22
|
+
basket_id=i1.id,
|
|
23
|
+
basket_content_type_id=basket_content_type_id,
|
|
24
|
+
key=key,
|
|
25
|
+
metrics={"a": "a"},
|
|
26
|
+
date=weekday,
|
|
27
|
+
)
|
|
28
|
+
level1_dto_metric = Metric(
|
|
29
|
+
basket_id=i2.id,
|
|
30
|
+
basket_content_type_id=basket_content_type_id,
|
|
31
|
+
key=key,
|
|
32
|
+
metrics={"b": "b"},
|
|
33
|
+
date=weekday,
|
|
34
|
+
dependency_metrics=[level1_dto_metric],
|
|
35
|
+
)
|
|
36
|
+
base_dto_metric = Metric(
|
|
37
|
+
basket_id=i3.id,
|
|
38
|
+
basket_content_type_id=basket_content_type_id,
|
|
39
|
+
key=key,
|
|
40
|
+
metrics={"c": "c"},
|
|
41
|
+
date=weekday,
|
|
42
|
+
dependency_metrics=[level1_dto_metric],
|
|
43
|
+
)
|
|
44
|
+
InstrumentMetric.update_or_create_from_metric(base_dto_metric)
|
|
45
|
+
|
|
46
|
+
level2_metric = InstrumentMetric.objects.get(
|
|
47
|
+
basket_id=i1.id, basket_content_type_id=basket_content_type_id, date=weekday, key=key
|
|
48
|
+
)
|
|
49
|
+
level1_metric = InstrumentMetric.objects.get(
|
|
50
|
+
basket_id=i2.id, basket_content_type_id=basket_content_type_id, date=weekday, key=key
|
|
51
|
+
)
|
|
52
|
+
base_metric = InstrumentMetric.objects.get(
|
|
53
|
+
basket_id=i3.id, basket_content_type_id=basket_content_type_id, date=weekday, key=key
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert level2_metric.metrics == {"a": "a"}
|
|
57
|
+
assert level2_metric.parent_metric == level1_metric
|
|
58
|
+
|
|
59
|
+
assert level1_metric.metrics == {"b": "b"}
|
|
60
|
+
assert level1_metric.parent_metric == base_metric
|
|
61
|
+
|
|
62
|
+
assert base_metric.metrics == {"c": "c"}
|
|
63
|
+
|
|
64
|
+
def test_annotate_with_metrics(self, instrument_metric):
|
|
65
|
+
qs = Instrument.objects.filter(id=instrument_metric.basket_id)
|
|
66
|
+
qs = InstrumentMetric.annotate_with_metrics(
|
|
67
|
+
qs, backend_registry._metric_key_map[instrument_metric.key], Instrument, val_date=instrument_metric.date
|
|
68
|
+
)
|
|
69
|
+
for field in instrument_metric.metrics.keys():
|
|
70
|
+
qs_field = instrument_metric.key + "_" + field
|
|
71
|
+
res = list(qs.values_list(qs_field, flat=True))[0]
|
|
72
|
+
assert res == instrument_metric.metrics[field]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from faker import Faker
|
|
4
|
+
|
|
5
|
+
from ..dispatch import compute_metrics
|
|
6
|
+
from ..orchestrators import MetricOrchestrator
|
|
7
|
+
from ..tasks import compute_metrics_as_task
|
|
8
|
+
|
|
9
|
+
fake = Faker()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@patch.object(MetricOrchestrator, "process")
|
|
13
|
+
def test_dispatch_compute_metrics(mock_fct):
|
|
14
|
+
key = "performance"
|
|
15
|
+
basket = object()
|
|
16
|
+
val_date = fake.date_object()
|
|
17
|
+
compute_metrics(val_date, key=key, basket=basket)
|
|
18
|
+
mock_fct.assert_called_once()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@patch.object(MetricOrchestrator, "process")
|
|
22
|
+
def test_periodically_compute_all_metrics(mock_fct):
|
|
23
|
+
compute_metrics_as_task()
|
|
24
|
+
mock_fct.assert_called_once()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from faker import Faker
|
|
5
|
+
from rest_framework.test import APIRequestFactory
|
|
6
|
+
from wbcore import viewsets
|
|
7
|
+
from wbcore.utils.strings import get_aggregate_symbol
|
|
8
|
+
from wbfdm.contrib.metric.backends.performances import (
|
|
9
|
+
PERFORMANCE_METRIC,
|
|
10
|
+
InstrumentPerformanceMetricBackend,
|
|
11
|
+
)
|
|
12
|
+
from wbfdm.contrib.metric.models import InstrumentMetric
|
|
13
|
+
from wbfdm.models import Instrument
|
|
14
|
+
from wbfdm.serializers import InstrumentModelSerializer
|
|
15
|
+
|
|
16
|
+
from ..viewsets.mixins import InstrumentMetricMixin
|
|
17
|
+
|
|
18
|
+
fake = Faker()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.django_db
|
|
22
|
+
class TestInstrumentMetricMixin:
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def viewset(self, instrument_metric_factory):
|
|
25
|
+
metric = instrument_metric_factory.create(key="performance")
|
|
26
|
+
|
|
27
|
+
class InstrumentWithMetricModelViewSet(InstrumentMetricMixin, viewsets.ModelViewSet):
|
|
28
|
+
METRIC_KEYS = (PERFORMANCE_METRIC,)
|
|
29
|
+
METRIC_WITH_PREFIXED_KEYS = fake.boolean()
|
|
30
|
+
serializer_class = InstrumentModelSerializer
|
|
31
|
+
queryset = Instrument.objects.all()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def metric_basket(self):
|
|
35
|
+
return metric.basket
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def metric_date(self) -> date | None:
|
|
39
|
+
return metric.date
|
|
40
|
+
|
|
41
|
+
request = APIRequestFactory().get("")
|
|
42
|
+
return InstrumentWithMetricModelViewSet(request=request, format_kwarg={})
|
|
43
|
+
|
|
44
|
+
def test_metric_serializer_fields(self, weekday, viewset):
|
|
45
|
+
assert (
|
|
46
|
+
viewset._metric_serializer_fields.keys()
|
|
47
|
+
== InstrumentPerformanceMetricBackend(weekday)
|
|
48
|
+
.get_serializer_fields(with_prefixed_key=viewset.METRIC_WITH_PREFIXED_KEYS)
|
|
49
|
+
.keys()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_get_ordering_fields(self, viewset):
|
|
53
|
+
ordering_fields = viewset.get_ordering_fields()
|
|
54
|
+
for key, _ in PERFORMANCE_METRIC.get_fields(with_prefixed_key=viewset.METRIC_WITH_PREFIXED_KEYS):
|
|
55
|
+
assert key in ordering_fields
|
|
56
|
+
|
|
57
|
+
def test_get_queryset(self, viewset):
|
|
58
|
+
queryset = viewset.get_queryset()
|
|
59
|
+
annotations = list(queryset.query.annotations.keys())
|
|
60
|
+
for key, _ in PERFORMANCE_METRIC.get_fields(with_prefixed_key=viewset.METRIC_WITH_PREFIXED_KEYS):
|
|
61
|
+
assert key in annotations
|
|
62
|
+
|
|
63
|
+
def test_get_serializer(self, viewset):
|
|
64
|
+
serializer = viewset.get_serializer()
|
|
65
|
+
for key, _ in PERFORMANCE_METRIC.get_fields(with_prefixed_key=viewset.METRIC_WITH_PREFIXED_KEYS):
|
|
66
|
+
assert key in serializer.fields
|
|
67
|
+
|
|
68
|
+
def test_get_aggregates(self, viewset):
|
|
69
|
+
queryset = viewset.get_queryset()
|
|
70
|
+
aggregates = viewset.get_aggregates(queryset, queryset)
|
|
71
|
+
metric = InstrumentMetric.objects.first()
|
|
72
|
+
assert metric is not None
|
|
73
|
+
for key, _ in PERFORMANCE_METRIC.get_fields(with_prefixed_key=viewset.METRIC_WITH_PREFIXED_KEYS):
|
|
74
|
+
key_filter = PERFORMANCE_METRIC.subfields_filter_map[key]
|
|
75
|
+
subfield = PERFORMANCE_METRIC.subfields_map[key]
|
|
76
|
+
assert subfield.aggregate is not None
|
|
77
|
+
|
|
78
|
+
if key_filter in metric.metrics:
|
|
79
|
+
assert aggregates[key] == {get_aggregate_symbol(subfield.aggregate.name): metric.metrics[key_filter]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.urls import include, path
|
|
2
|
+
from wbcore.routers import WBCoreRouter
|
|
3
|
+
|
|
4
|
+
from . import viewsets
|
|
5
|
+
|
|
6
|
+
router = WBCoreRouter()
|
|
7
|
+
|
|
8
|
+
# Representations
|
|
9
|
+
router.register(
|
|
10
|
+
r"instrumentmetricrepresentation",
|
|
11
|
+
viewsets.InstrumentMetricRepresentationViewSet,
|
|
12
|
+
basename="instrumentmetricrepresentation",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
router.register(r"instrumentmetric", viewsets.InstrumentMetricViewSet, basename="instrumentmetric")
|
|
16
|
+
|
|
17
|
+
urlpatterns = [
|
|
18
|
+
path("", include(router.urls)),
|
|
19
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .viewsets import InstrumentMetricRepresentationViewSet, InstrumentMetricViewSet
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .display import InstrumentMetricDisplayConfig
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from rest_framework.reverse import reverse
|
|
4
|
+
from wbcore.metadata.configs import display as dp
|
|
5
|
+
from wbcore.metadata.configs.display.instance_display import Display
|
|
6
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
7
|
+
create_simple_display,
|
|
8
|
+
create_simple_section,
|
|
9
|
+
)
|
|
10
|
+
from wbcore.metadata.configs.display.instance_display.utils import repeat_field
|
|
11
|
+
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
12
|
+
from wbfdm.contrib.metric.dto import MetricField
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstrumentMetricDisplayConfig(DisplayViewConfig):
|
|
16
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
17
|
+
return dp.ListDisplay(
|
|
18
|
+
fields=[
|
|
19
|
+
dp.Field(key="key", label="Key"),
|
|
20
|
+
dp.Field(key="date", label="Date"),
|
|
21
|
+
dp.Field(key="instrument", label="Instrument"),
|
|
22
|
+
dp.Field(key="parent_metric", label="Parent Metric"),
|
|
23
|
+
],
|
|
24
|
+
tree=True,
|
|
25
|
+
tree_group_pinned="left",
|
|
26
|
+
tree_group_field="basket_repr",
|
|
27
|
+
tree_group_label="Basket",
|
|
28
|
+
tree_group_level_options=[
|
|
29
|
+
dp.TreeGroupLevelOption(
|
|
30
|
+
filter_key="parent_metric",
|
|
31
|
+
filter_depth=1,
|
|
32
|
+
# lookup="id_repr",
|
|
33
|
+
clear_filter=True,
|
|
34
|
+
list_endpoint=reverse(
|
|
35
|
+
"metric:instrumentmetric-list",
|
|
36
|
+
args=[],
|
|
37
|
+
request=self.request,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get_instance_display(self) -> Display:
|
|
44
|
+
children_metrics_section = create_simple_section(
|
|
45
|
+
"children_metrics_section", "Child Metrics", [["children_metrics"]], "children_metrics", collapsed=True
|
|
46
|
+
)
|
|
47
|
+
return create_simple_display(
|
|
48
|
+
[
|
|
49
|
+
["basket_content_type", "basket_id", "basket_repr"],
|
|
50
|
+
["date", "key", "instrument"],
|
|
51
|
+
[repeat_field(3, "metrics")],
|
|
52
|
+
[repeat_field(3, "children_metrics")],
|
|
53
|
+
],
|
|
54
|
+
[children_metrics_section],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class InstrumentMetricPivotedListDisplayConfig(DisplayViewConfig):
|
|
59
|
+
"""
|
|
60
|
+
Instrument metric Class to register automatically metrics fields into the list display.
|
|
61
|
+
|
|
62
|
+
The view attribute is expected to inherit from InstrumentMetricMixin
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def _get_metric_field_attr(self, field: MetricField) -> dict[str, Any]:
|
|
66
|
+
attrs = dict(width=75)
|
|
67
|
+
attrs.update(field.list_display_kwargs)
|
|
68
|
+
return attrs
|
|
69
|
+
|
|
70
|
+
def _get_metric_list_display(self) -> dp.ListDisplay:
|
|
71
|
+
metrics = []
|
|
72
|
+
|
|
73
|
+
for metric_key in self.view.metric_keys:
|
|
74
|
+
metric_label = metric_key.label
|
|
75
|
+
fields = []
|
|
76
|
+
for subfield in metric_key.subfields:
|
|
77
|
+
field_key = f"{metric_key.key}_{subfield.key}"
|
|
78
|
+
fields.append(dp.Field(key=field_key, label=subfield.label, **self._get_metric_field_attr(subfield)))
|
|
79
|
+
metrics.append(dp.Field(key=None, label=metric_label, children=fields))
|
|
80
|
+
|
|
81
|
+
return dp.ListDisplay(fields=[dp.Field(label="Metrics", open_by_default=False, key=None, children=metrics)])
|
|
82
|
+
|
|
83
|
+
def _get_metadata(self) -> Any:
|
|
84
|
+
display = self.get_metadata()
|
|
85
|
+
if "list" in display:
|
|
86
|
+
list_display = self._get_metric_list_display()
|
|
87
|
+
if isinstance(list_display, Display):
|
|
88
|
+
metrics_list_display = list_display.serialize()
|
|
89
|
+
else:
|
|
90
|
+
metrics_list_display = dict(list_display or {})
|
|
91
|
+
display["list"]["fields"].extend(metrics_list_display["fields"])
|
|
92
|
+
return display
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from wbcore.menus import ItemPermission, MenuItem
|
|
2
|
+
from wbcore.permissions.shortcuts import is_internal_user
|
|
3
|
+
|
|
4
|
+
INSTRUMENTMETRIC_MENUITEM = MenuItem(
|
|
5
|
+
label="Metrics",
|
|
6
|
+
endpoint="metric:instrumentmetric-list",
|
|
7
|
+
endpoint_get_parameters={"parent_metric__isnull": True},
|
|
8
|
+
permission=ItemPermission(
|
|
9
|
+
permissions=["metric.view_instrumentmetric"], method=lambda request: is_internal_user(request.user)
|
|
10
|
+
),
|
|
11
|
+
)
|