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,120 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from slugify import slugify
|
|
3
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
4
|
+
from wbcore.models import WBModel
|
|
5
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
6
|
+
from wbfdm.import_export.handlers.instrument_list import InstrumentListImportHandler
|
|
7
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstrumentListThroughModel(ImportMixin, ComplexToStringMixin):
|
|
11
|
+
import_export_handler_class = InstrumentListImportHandler
|
|
12
|
+
"""
|
|
13
|
+
This model is not a Through model from a programming point of view, however it allows to link instrument list to
|
|
14
|
+
instruments.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
instrument_str = models.CharField(max_length=256)
|
|
18
|
+
instrument = models.ForeignKey(
|
|
19
|
+
to="wbfdm.Instrument",
|
|
20
|
+
null=True,
|
|
21
|
+
blank=True,
|
|
22
|
+
limit_choices_to=models.Q(is_security=True),
|
|
23
|
+
on_delete=models.SET_NULL,
|
|
24
|
+
)
|
|
25
|
+
instrument_list = models.ForeignKey(
|
|
26
|
+
to="wbfdm.InstrumentList",
|
|
27
|
+
on_delete=models.CASCADE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from_date = models.DateField(null=True, blank=True)
|
|
31
|
+
to_date = models.DateField(null=True, blank=True)
|
|
32
|
+
comment = models.TextField(default="", blank=True)
|
|
33
|
+
validated = models.BooleanField(default=False)
|
|
34
|
+
|
|
35
|
+
def compute_str(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Method to compute the string representation of the instance. It will save the string value to the computed_str
|
|
38
|
+
field.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The string representation of the instance.
|
|
42
|
+
"""
|
|
43
|
+
if self.instrument and self.instrument.name_repr:
|
|
44
|
+
return f"{self.instrument.name_repr} - {self.instrument_list.name}"
|
|
45
|
+
return f"{self.instrument_str} - {self.instrument_list.name}"
|
|
46
|
+
|
|
47
|
+
class Meta:
|
|
48
|
+
verbose_name = "Instrument in Instrument List"
|
|
49
|
+
constraints = [
|
|
50
|
+
models.UniqueConstraint(fields=["instrument", "instrument_list"], name="unique_instrument_per_list")
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
notification_types = [
|
|
54
|
+
(
|
|
55
|
+
"wbfdm.instrument_list_add",
|
|
56
|
+
"Instrument added to Instrument List",
|
|
57
|
+
"A notification when an instrument gets added to a list.",
|
|
58
|
+
True,
|
|
59
|
+
True,
|
|
60
|
+
True,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class InstrumentList(WBModel):
|
|
66
|
+
class InstrumentListType(models.TextChoices):
|
|
67
|
+
WATCH = "WATCH", "Watch List"
|
|
68
|
+
EXCLUSION = "EXCLUSION", "Exclusion List"
|
|
69
|
+
|
|
70
|
+
name = models.CharField(max_length=255)
|
|
71
|
+
identifier = models.CharField(max_length=255, unique=True, blank=True)
|
|
72
|
+
instrument_list_type = models.CharField(max_length=32, choices=InstrumentListType.choices, null=True, blank=True)
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return self.name
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def instruments(self) -> models.QuerySet[Instrument]:
|
|
79
|
+
"""
|
|
80
|
+
Returns a QuerySet of Instrument objects associated with the current instrument list.
|
|
81
|
+
|
|
82
|
+
This property filters the Instrument objects based on the related InstrumentListThroughModel
|
|
83
|
+
and returns only those Instruments where the foreign key is not null.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
models.QuerySet[Instrument]: A QuerySet of Instrument objects.
|
|
87
|
+
"""
|
|
88
|
+
return Instrument.objects.filter(
|
|
89
|
+
id__in=(
|
|
90
|
+
InstrumentListThroughModel.objects.filter(instrument_list=self, instrument__isnull=False).values(
|
|
91
|
+
"instrument"
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def save(self, *args, **kwargs):
|
|
97
|
+
if not self.identifier:
|
|
98
|
+
self.identifier = slugify(f"{self.name}-{self.id}")
|
|
99
|
+
super().save(*args, **kwargs)
|
|
100
|
+
|
|
101
|
+
class Meta:
|
|
102
|
+
verbose_name = "Instrument List"
|
|
103
|
+
verbose_name_plural = "Instrument Lists"
|
|
104
|
+
permissions = (("administrate_instrumentlist", "Can administrate Instrument List"),)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def get_endpoint_basename(cls):
|
|
108
|
+
return "wbfdm:instrumentlist"
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_representation_endpoint(cls):
|
|
112
|
+
return "wbfdm:instrumentlistrepresentation-list"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_representation_value_key(cls):
|
|
116
|
+
return "id"
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_representation_label_key(cls):
|
|
120
|
+
return "{{name}}"
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from celery import shared_task
|
|
6
|
+
from django.db import models, transaction
|
|
7
|
+
from django.db.models import (
|
|
8
|
+
Case,
|
|
9
|
+
DecimalField,
|
|
10
|
+
Exists,
|
|
11
|
+
ExpressionWrapper,
|
|
12
|
+
F,
|
|
13
|
+
OuterRef,
|
|
14
|
+
Q,
|
|
15
|
+
QuerySet,
|
|
16
|
+
Subquery,
|
|
17
|
+
Value,
|
|
18
|
+
When,
|
|
19
|
+
)
|
|
20
|
+
from django.db.models.signals import post_save
|
|
21
|
+
from django.dispatch import receiver
|
|
22
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
23
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
24
|
+
from wbcore.models import DynamicDecimalField, DynamicFloatField, DynamicModel, WBModel
|
|
25
|
+
from wbcore.signals import pre_merge
|
|
26
|
+
from wbfdm.analysis.financial_analysis.financial_statistics_analysis import (
|
|
27
|
+
FinancialStatistics,
|
|
28
|
+
)
|
|
29
|
+
from wbfdm.import_export.handlers.instrument_price import InstrumentPriceImportHandler
|
|
30
|
+
|
|
31
|
+
from .mixin.financials_computed import InstrumentPriceComputedMixin
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ValidPricesQueryset(QuerySet):
|
|
35
|
+
def filter_only_valid_prices(self) -> QuerySet:
|
|
36
|
+
"""
|
|
37
|
+
Filter the queryset to remove duplicate in case calculated and non-calculated prices are present for the same date/product/type
|
|
38
|
+
"""
|
|
39
|
+
return self.annotate(
|
|
40
|
+
real_price_exists=Exists(
|
|
41
|
+
self.filter(
|
|
42
|
+
instrument=OuterRef("instrument"),
|
|
43
|
+
date=OuterRef("date"),
|
|
44
|
+
calculated=False,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
).filter(Q(calculated=False) | (Q(real_price_exists=False) & Q(calculated=True)))
|
|
48
|
+
|
|
49
|
+
def annotate_market_data(self):
|
|
50
|
+
base_qs = ValidPricesQueryset(self.model, using=self._db)
|
|
51
|
+
|
|
52
|
+
return self.annotate(
|
|
53
|
+
currency_fx_rate_to_usd_rate=F("currency_fx_rate_to_usd__value"),
|
|
54
|
+
calculated_outstanding_shares=Subquery(
|
|
55
|
+
base_qs.filter(instrument=OuterRef("instrument"), calculated=True, date=OuterRef("date")).values(
|
|
56
|
+
"outstanding_shares"
|
|
57
|
+
)[:1]
|
|
58
|
+
),
|
|
59
|
+
internal_outstanding_shares=ExpressionWrapper(
|
|
60
|
+
Case( # Annotate the parent security if exists
|
|
61
|
+
When(
|
|
62
|
+
outstanding_shares__isnull=False,
|
|
63
|
+
then=F("outstanding_shares"),
|
|
64
|
+
),
|
|
65
|
+
default=F("calculated_outstanding_shares"),
|
|
66
|
+
),
|
|
67
|
+
output_field=DecimalField(max_digits=4),
|
|
68
|
+
),
|
|
69
|
+
internal_market_capitalization=Case(
|
|
70
|
+
When(
|
|
71
|
+
market_capitalization__isnull=True,
|
|
72
|
+
then=ExpressionWrapper(
|
|
73
|
+
F("internal_outstanding_shares") * F("net_value"),
|
|
74
|
+
output_field=DecimalField(max_digits=4),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
default=ExpressionWrapper(F("market_capitalization"), output_field=DecimalField(max_digits=4)),
|
|
78
|
+
),
|
|
79
|
+
internal_market_capitalization_usd=F("internal_market_capitalization") / F("currency_fx_rate_to_usd_rate"),
|
|
80
|
+
calculated_volume=Subquery(
|
|
81
|
+
base_qs.filter(instrument=OuterRef("instrument"), calculated=True, date=OuterRef("date")).values(
|
|
82
|
+
"volume"
|
|
83
|
+
)[:1]
|
|
84
|
+
),
|
|
85
|
+
internal_volume=ExpressionWrapper(
|
|
86
|
+
Case( # Annotate the parent security if exists
|
|
87
|
+
When(
|
|
88
|
+
volume__isnull=False,
|
|
89
|
+
then=F("volume"),
|
|
90
|
+
),
|
|
91
|
+
default=F("calculated_volume"),
|
|
92
|
+
),
|
|
93
|
+
output_field=models.FloatField(),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def annotate_base_data(self):
|
|
98
|
+
return self.annotate(
|
|
99
|
+
currency_fx_rate_to_usd_rate=F("currency_fx_rate_to_usd__value"),
|
|
100
|
+
market_capitalization_usd=ExpressionWrapper(
|
|
101
|
+
F("market_capitalization") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField(max_digits=4)
|
|
102
|
+
),
|
|
103
|
+
net_value_usd=F("net_value") / F("currency_fx_rate_to_usd_rate"),
|
|
104
|
+
gross_value_usd=F("gross_value") / F("currency_fx_rate_to_usd_rate"),
|
|
105
|
+
volume_usd=ExpressionWrapper(
|
|
106
|
+
F("volume") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
|
|
107
|
+
),
|
|
108
|
+
volume_50d_usd=ExpressionWrapper(
|
|
109
|
+
F("volume_50d") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
|
|
110
|
+
),
|
|
111
|
+
volume_200d_usd=ExpressionWrapper(
|
|
112
|
+
F("volume_200d") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def annotate_security_data(self):
|
|
117
|
+
return self.annotate(
|
|
118
|
+
security=Case( # Annotate the parent security if exists
|
|
119
|
+
When(instrument__parent__isnull=False, then=F("instrument__parent")),
|
|
120
|
+
default=F("instrument"),
|
|
121
|
+
),
|
|
122
|
+
security_instrument_type_key=Case( # Annotate the parent security if exists
|
|
123
|
+
When(
|
|
124
|
+
instrument__parent__isnull=False,
|
|
125
|
+
then=F("instrument__parent__instrument_type__key"),
|
|
126
|
+
),
|
|
127
|
+
default=F("instrument__instrument_type__key"),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def annotate_all(self):
|
|
132
|
+
return self.annotate_market_data().annotate_security_data().annotate_base_data()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class InstrumentPriceManager(models.Manager):
|
|
136
|
+
def __init__(self, with_annotation: bool = False, *args, **kwargs):
|
|
137
|
+
self.with_annotation = with_annotation
|
|
138
|
+
super().__init__(*args, **kwargs)
|
|
139
|
+
|
|
140
|
+
def get_queryset(self) -> ValidPricesQueryset:
|
|
141
|
+
qs = ValidPricesQueryset(self.model)
|
|
142
|
+
if self.with_annotation:
|
|
143
|
+
qs = qs.annotate_all()
|
|
144
|
+
return qs
|
|
145
|
+
|
|
146
|
+
def filtered_by_instruments(self, instrument_queryset, *other_instruments):
|
|
147
|
+
return self.filter(models.Q(instrument__in=instrument_queryset) | models.Q(instrument__in=other_instruments))
|
|
148
|
+
|
|
149
|
+
def filter_only_valid_prices(self) -> QuerySet:
|
|
150
|
+
return self.get_queryset().filter_only_valid_prices()
|
|
151
|
+
|
|
152
|
+
def annotate_market_data(self) -> QuerySet:
|
|
153
|
+
return self.get_queryset().annotate_market_data()
|
|
154
|
+
|
|
155
|
+
def annotate_base_data(self) -> QuerySet:
|
|
156
|
+
return self.get_queryset().annotate_base_data()
|
|
157
|
+
|
|
158
|
+
def annotate_security_data(self) -> QuerySet:
|
|
159
|
+
return self.get_queryset().annotate_security_data()
|
|
160
|
+
|
|
161
|
+
def annotate_all(self) -> QuerySet:
|
|
162
|
+
return self.get_queryset().annotate_all()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AnnotatedInstrumentPriceManager(InstrumentPriceManager):
|
|
166
|
+
def get_queryset(self):
|
|
167
|
+
return super().get_queryset().annotate_market_data().annotate_base_data().annotate_security_data()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class InstrumentPrice(
|
|
171
|
+
ImportMixin,
|
|
172
|
+
InstrumentPriceComputedMixin,
|
|
173
|
+
DynamicModel,
|
|
174
|
+
WBModel,
|
|
175
|
+
):
|
|
176
|
+
import_export_handler_class = InstrumentPriceImportHandler
|
|
177
|
+
# Base fields
|
|
178
|
+
instrument = models.ForeignKey(
|
|
179
|
+
to="wbfdm.Instrument",
|
|
180
|
+
related_name="prices",
|
|
181
|
+
on_delete=models.PROTECT,
|
|
182
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
183
|
+
verbose_name="Instrument",
|
|
184
|
+
blank=True,
|
|
185
|
+
null=True,
|
|
186
|
+
)
|
|
187
|
+
date = models.DateField(verbose_name="Date")
|
|
188
|
+
calculated = models.BooleanField(default=False, verbose_name="Is Calculated")
|
|
189
|
+
|
|
190
|
+
net_value = models.DecimalField(max_digits=16, decimal_places=6, verbose_name="Value (Net)")
|
|
191
|
+
gross_value = DynamicDecimalField(
|
|
192
|
+
max_digits=16,
|
|
193
|
+
decimal_places=6,
|
|
194
|
+
verbose_name="Value (Gross)",
|
|
195
|
+
) # TODO: I think we need to remove this field that is not really used here.
|
|
196
|
+
|
|
197
|
+
outstanding_shares = DynamicDecimalField(
|
|
198
|
+
decimal_places=4,
|
|
199
|
+
max_digits=16,
|
|
200
|
+
verbose_name="Outstanding Shares",
|
|
201
|
+
help_text="The amount of outstanding share for this instrument",
|
|
202
|
+
)
|
|
203
|
+
outstanding_shares_consolidated = DynamicDecimalField(
|
|
204
|
+
decimal_places=4,
|
|
205
|
+
max_digits=16,
|
|
206
|
+
verbose_name="Outstanding Shares (Consolidated)",
|
|
207
|
+
help_text="The amount of outstanding share for this instrument",
|
|
208
|
+
)
|
|
209
|
+
########################################################
|
|
210
|
+
# ASSET STATISTICS #
|
|
211
|
+
########################################################
|
|
212
|
+
|
|
213
|
+
volume = models.FloatField(
|
|
214
|
+
null=True,
|
|
215
|
+
blank=True,
|
|
216
|
+
verbose_name="Volume",
|
|
217
|
+
help_text="The Volume of the Asset on the price date of the Asset.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
volume_50d = DynamicFloatField(
|
|
221
|
+
verbose_name="Average Volume (50 Days)",
|
|
222
|
+
help_text="The Average Volume of the Asset over the last 50 days from the price date of the Asset.",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
volume_200d = models.FloatField(
|
|
226
|
+
null=True,
|
|
227
|
+
blank=True,
|
|
228
|
+
verbose_name="Average Volume (200 Days)",
|
|
229
|
+
help_text="The Average Volume of the Asset over the last 200 days from the price date of the Asset.",
|
|
230
|
+
)
|
|
231
|
+
market_capitalization = models.FloatField(
|
|
232
|
+
null=True,
|
|
233
|
+
blank=True,
|
|
234
|
+
verbose_name="Market Capitalization",
|
|
235
|
+
help_text="The Market Capitalization of the Asset the price date of the Asset.",
|
|
236
|
+
)
|
|
237
|
+
market_capitalization_consolidated = models.FloatField(
|
|
238
|
+
null=True,
|
|
239
|
+
blank=True,
|
|
240
|
+
verbose_name="Market Capitalization (Consolidated)",
|
|
241
|
+
help_text="the consolidated market value of a company in local currency.",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
currency_fx_rate_to_usd = models.ForeignKey(
|
|
245
|
+
to="currency.CurrencyFXRates",
|
|
246
|
+
related_name="instrument_prices",
|
|
247
|
+
on_delete=models.PROTECT,
|
|
248
|
+
blank=True,
|
|
249
|
+
null=True,
|
|
250
|
+
verbose_name="Instrument Currency Rate",
|
|
251
|
+
help_text="Rate to between instrument currency and USD",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Statistics
|
|
255
|
+
lock_statistics = models.BooleanField(
|
|
256
|
+
default=False,
|
|
257
|
+
help_text="If True, a save will not override the beta, correlation and sharpe ratio",
|
|
258
|
+
)
|
|
259
|
+
sharpe_ratio = models.FloatField(blank=True, null=True, verbose_name="Sharpe Ratio")
|
|
260
|
+
correlation = models.FloatField(blank=True, null=True, verbose_name="Correlation")
|
|
261
|
+
beta = models.FloatField(blank=True, null=True, verbose_name="Beta")
|
|
262
|
+
|
|
263
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
|
|
264
|
+
modified = models.DateTimeField(
|
|
265
|
+
verbose_name="Modified",
|
|
266
|
+
auto_now=True,
|
|
267
|
+
)
|
|
268
|
+
# custom_beta_180d = DynamicFloatField(verbose_name="Custom Beta (180 days)")
|
|
269
|
+
# custom_beta_1y = DynamicFloatField(verbose_name="Custom Beta (1 Years)")
|
|
270
|
+
# custom_beta_2y = DynamicFloatField(verbose_name="Custom Beta (2 Years)")
|
|
271
|
+
# custom_beta_3y = DynamicFloatField(verbose_name="Custom Beta (3 Years)")
|
|
272
|
+
# custom_beta_5y = DynamicFloatField(verbose_name="Custom Beta (4 Years)")
|
|
273
|
+
#
|
|
274
|
+
# # Performances fields
|
|
275
|
+
# performance_1d = DynamicDecimalField(
|
|
276
|
+
# verbose_name="Performance 1D", help_text="Performance 1D", max_digits=16, decimal_places=6
|
|
277
|
+
# )
|
|
278
|
+
# performance_7d = DynamicDecimalField(
|
|
279
|
+
# verbose_name="Performance (1W)", help_text="Performance 7 days rolling", max_digits=16, decimal_places=6
|
|
280
|
+
# )
|
|
281
|
+
# performance_30d = DynamicDecimalField(
|
|
282
|
+
# verbose_name="Performance (1M)", help_text="Performance 30 days rolling", max_digits=16, decimal_places=6
|
|
283
|
+
# )
|
|
284
|
+
# performance_90d = DynamicDecimalField(
|
|
285
|
+
# verbose_name="Performance (1Q)", help_text="Performance 90 days rolling", max_digits=16, decimal_places=6
|
|
286
|
+
# )
|
|
287
|
+
# performance_365d = DynamicDecimalField(
|
|
288
|
+
# verbose_name="Performance (1Y)", help_text="Performance 365 days rolling", max_digits=16, decimal_places=6
|
|
289
|
+
# )
|
|
290
|
+
# performance_ytd = DynamicDecimalField(
|
|
291
|
+
# verbose_name="Performance (YTD)", help_text="Performance Year-to-date", max_digits=16, decimal_places=6
|
|
292
|
+
# )
|
|
293
|
+
# performance_inception = DynamicDecimalField(
|
|
294
|
+
# verbose_name="Performance (Inception)",
|
|
295
|
+
# help_text="Performance since inception",
|
|
296
|
+
# max_digits=16,
|
|
297
|
+
# decimal_places=6,
|
|
298
|
+
# )
|
|
299
|
+
|
|
300
|
+
objects = InstrumentPriceManager()
|
|
301
|
+
annotated_objects = InstrumentPriceManager(with_annotation=True)
|
|
302
|
+
|
|
303
|
+
class Meta:
|
|
304
|
+
verbose_name = "Instrument Price"
|
|
305
|
+
verbose_name_plural = "Instrument Prices"
|
|
306
|
+
constraints = [
|
|
307
|
+
models.CheckConstraint(
|
|
308
|
+
check=~models.Q(date__week_day__in=[1, 7]),
|
|
309
|
+
name="%(app_label)s_%(class)s_weekday_constraint",
|
|
310
|
+
),
|
|
311
|
+
models.UniqueConstraint(fields=["instrument", "date", "calculated"], name="unique_price"),
|
|
312
|
+
]
|
|
313
|
+
indexes = [
|
|
314
|
+
models.Index(
|
|
315
|
+
name="fdm_instrumentprice_base_idx",
|
|
316
|
+
fields=["calculated", "date", "instrument"],
|
|
317
|
+
),
|
|
318
|
+
models.Index(
|
|
319
|
+
name="fdm_instrumentprice_idx1",
|
|
320
|
+
fields=["calculated", "instrument"],
|
|
321
|
+
),
|
|
322
|
+
models.Index(
|
|
323
|
+
name="fdm_instrumentprice_idx2",
|
|
324
|
+
fields=["instrument"],
|
|
325
|
+
),
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def _net_value_usd(self):
|
|
330
|
+
if self.currency_fx_rate_to_usd:
|
|
331
|
+
return getattr(self, "net_value_usd", self.net_value * self.currency_fx_rate_to_usd.value)
|
|
332
|
+
|
|
333
|
+
def save(self, *args, **kwargs):
|
|
334
|
+
if not self.currency_fx_rate_to_usd:
|
|
335
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
336
|
+
self.currency_fx_rate_to_usd = CurrencyFXRates.objects.get(
|
|
337
|
+
date=self.date, currency=self.instrument.currency
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if self.market_capitalization_consolidated is None:
|
|
341
|
+
self.market_capitalization_consolidated = self.market_capitalization
|
|
342
|
+
super().save(*args, **kwargs)
|
|
343
|
+
|
|
344
|
+
def __str__(self):
|
|
345
|
+
return f"{self.instrument.name}: {self.net_value} {self.date:%d.%m.%Y}"
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def previous_price(self):
|
|
349
|
+
"""Returns the previous instrument prices if one exists or None
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
instrument.InstrumentPrice | None -- Previous InstrumentPrice
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
return InstrumentPrice.objects.filter(
|
|
356
|
+
instrument=self.instrument, date__lt=self.date, calculated=self.calculated
|
|
357
|
+
).latest("date")
|
|
358
|
+
except InstrumentPrice.DoesNotExist:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def next_price(self):
|
|
363
|
+
"""Returns the next instrument prices if one exists or None
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
instrument.InstrumentPrice | None -- Next InstrumentPrice
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
return self.instrument.prices.filter(date__gt=self.date, calculated=self.calculated).earliest("date")
|
|
370
|
+
except InstrumentPrice.DoesNotExist:
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def shares(self):
|
|
375
|
+
"""Returns the number of shares of a instrument
|
|
376
|
+
|
|
377
|
+
The number of shares are the sum of all customer trades
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
int -- Shares
|
|
381
|
+
"""
|
|
382
|
+
return self.instrument.total_shares(self.date)
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def valid_outstanding_shares(self):
|
|
386
|
+
prices = self.instrument.prices.filter(date=self.date, outstanding_shares__isnull=False).order_by("calculated")
|
|
387
|
+
return prices.last().outstanding_shares if prices.exists() else self.shares
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def nominal_value(self):
|
|
391
|
+
"""Returns the nominal value of a instrument
|
|
392
|
+
|
|
393
|
+
The nominal value is the number of current shares multiplied by the share price of a instrument
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
int -- Nominal Value
|
|
397
|
+
"""
|
|
398
|
+
return self.instrument.nominal_value(self.date)
|
|
399
|
+
|
|
400
|
+
def compute_and_update_statistics(self, min_period: int = 20):
|
|
401
|
+
prices_df = (
|
|
402
|
+
pd.DataFrame(
|
|
403
|
+
InstrumentPrice.objects.filter_only_valid_prices()
|
|
404
|
+
.filter(instrument=self.instrument, date__lte=self.date)
|
|
405
|
+
.values_list("date", "net_value"),
|
|
406
|
+
columns=["date", "net_value"],
|
|
407
|
+
)
|
|
408
|
+
.set_index("date")
|
|
409
|
+
.sort_index()["net_value"]
|
|
410
|
+
)
|
|
411
|
+
if not prices_df.empty and prices_df.shape[0] >= min_period:
|
|
412
|
+
financial_statistics = FinancialStatistics(prices_df)
|
|
413
|
+
min_date = prices_df.index.min()
|
|
414
|
+
if risk_free_rate := self.instrument.primary_risk_instrument:
|
|
415
|
+
risk_free_rate_df = (
|
|
416
|
+
pd.DataFrame(
|
|
417
|
+
risk_free_rate.valuations.filter(date__gte=min_date, date__lte=self.date).values_list(
|
|
418
|
+
"date", "net_value"
|
|
419
|
+
),
|
|
420
|
+
columns=["date", "net_value"],
|
|
421
|
+
)
|
|
422
|
+
.set_index("date")
|
|
423
|
+
.sort_index()["net_value"]
|
|
424
|
+
)
|
|
425
|
+
if sharpe_ratio := financial_statistics.get_sharpe_ratio(risk_free_rate_df):
|
|
426
|
+
self.sharpe_ratio = sharpe_ratio
|
|
427
|
+
if benchmark := self.instrument.primary_benchmark:
|
|
428
|
+
benchmark_df = (
|
|
429
|
+
pd.DataFrame(
|
|
430
|
+
benchmark.valuations.filter(date__gte=min_date, date__lte=self.date).values_list(
|
|
431
|
+
"date", "net_value"
|
|
432
|
+
),
|
|
433
|
+
columns=["date", "net_value"],
|
|
434
|
+
)
|
|
435
|
+
.set_index("date")
|
|
436
|
+
.sort_index()["net_value"]
|
|
437
|
+
)
|
|
438
|
+
if (beta := financial_statistics.get_beta(benchmark_df)) is not None:
|
|
439
|
+
self.beta = beta
|
|
440
|
+
if (correlation := financial_statistics.get_correlation(benchmark_df)) is not None:
|
|
441
|
+
self.correlation = correlation
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def subquery_closest_value(
|
|
445
|
+
cls,
|
|
446
|
+
field_name,
|
|
447
|
+
val_date=None,
|
|
448
|
+
date_name="date",
|
|
449
|
+
instrument_pk_name="instrument__pk",
|
|
450
|
+
date_lookup="lte",
|
|
451
|
+
order_by="-date",
|
|
452
|
+
calculated_filter_value=False,
|
|
453
|
+
):
|
|
454
|
+
index_filter_params = {}
|
|
455
|
+
if calculated_filter_value is not None:
|
|
456
|
+
index_filter_params["calculated"] = calculated_filter_value
|
|
457
|
+
if not val_date and date_name:
|
|
458
|
+
index_filter_params[f"date__{date_lookup}"] = models.OuterRef(date_name)
|
|
459
|
+
elif val_date:
|
|
460
|
+
index_filter_params[f"date__{date_lookup}"] = val_date
|
|
461
|
+
index_filter_params["instrument"] = models.OuterRef(instrument_pk_name)
|
|
462
|
+
qs = cls.objects.filter(**index_filter_params).filter(**{f"{field_name}__isnull": False})
|
|
463
|
+
return models.Subquery(qs.order_by(order_by).values(field_name)[:1])
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
def annotate_sum_shares(cls, queryset, val_date, date_key="date"):
|
|
467
|
+
"""
|
|
468
|
+
Efficient way to annotate sum of shares without destroying indexing
|
|
469
|
+
"""
|
|
470
|
+
return queryset.annotate(
|
|
471
|
+
sum_shares_calculated=InstrumentPrice.subquery_closest_value(
|
|
472
|
+
"outstanding_shares",
|
|
473
|
+
date_name=date_key,
|
|
474
|
+
instrument_pk_name="pk",
|
|
475
|
+
date_lookup="lte",
|
|
476
|
+
order_by="-date",
|
|
477
|
+
calculated_filter_value=True,
|
|
478
|
+
), # We get the last price whose outstanding_shares is not none and date below date_key
|
|
479
|
+
sum_shares_real=InstrumentPrice.subquery_closest_value(
|
|
480
|
+
"outstanding_shares",
|
|
481
|
+
val_date=val_date,
|
|
482
|
+
date_name=date_key,
|
|
483
|
+
instrument_pk_name="pk",
|
|
484
|
+
date_lookup="exact",
|
|
485
|
+
calculated_filter_value=False,
|
|
486
|
+
),
|
|
487
|
+
sum_shares=Case(
|
|
488
|
+
When(sum_shares_real__isnull=False, then=F("sum_shares_real")),
|
|
489
|
+
When(sum_shares_calculated__isnull=False, then=F("sum_shares_calculated")),
|
|
490
|
+
default=Value(Decimal(0)),
|
|
491
|
+
output_field=DecimalField(),
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
def get_endpoint_basename(cls):
|
|
497
|
+
return "wbfdm:price"
|
|
498
|
+
|
|
499
|
+
@classmethod
|
|
500
|
+
def get_representation_value_key(cls):
|
|
501
|
+
return "id"
|
|
502
|
+
|
|
503
|
+
@classmethod
|
|
504
|
+
def get_representation_label_key(cls):
|
|
505
|
+
return "{{instrument} {{date}}"
|
|
506
|
+
|
|
507
|
+
@classmethod
|
|
508
|
+
def get_representation_endpoint(cls):
|
|
509
|
+
return "wbfdm:price-list"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@receiver(post_save, sender="currency.CurrencyFXRates")
|
|
513
|
+
def rate_creation(sender, instance, created, raw, **kwargs):
|
|
514
|
+
if not raw and created:
|
|
515
|
+
transaction.on_commit(lambda: update_currency_fx_rate_from_created_rate.delay(instance.id))
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
519
|
+
def pre_merge_instrument(sender: models.Model, merged_object, main_object, **kwargs):
|
|
520
|
+
"""
|
|
521
|
+
Simply reassign the prices of the merged instrument to the main instrument if they don't already exist for that day, otherwise simply delete them
|
|
522
|
+
"""
|
|
523
|
+
merged_object.prices.annotate(
|
|
524
|
+
already_exists=Exists(
|
|
525
|
+
InstrumentPrice.objects.filter(
|
|
526
|
+
calculated=OuterRef("calculated"), instrument=main_object, date=OuterRef("date")
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
).filter(already_exists=True).delete()
|
|
530
|
+
merged_object.prices.update(instrument=main_object)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@shared_task(queue="portfolio")
|
|
534
|
+
def update_currency_fx_rate_from_created_rate(rate_id):
|
|
535
|
+
currency_rate = CurrencyFXRates.objects.get(id=rate_id)
|
|
536
|
+
for price in InstrumentPrice.objects.filter(
|
|
537
|
+
instrument__currency=currency_rate.currency, date=currency_rate.date, currency_fx_rate_to_usd__isnull=True
|
|
538
|
+
):
|
|
539
|
+
price.currency_fx_rate_to_usd = currency_rate
|
|
540
|
+
price.save()
|