wbfdm 1.49.5__py2.py3-none-any.whl → 1.59.4__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.
Files changed (109) hide show
  1. wbfdm/admin/exchanges.py +1 -1
  2. wbfdm/admin/instruments.py +3 -2
  3. wbfdm/analysis/financial_analysis/change_point_detection.py +88 -0
  4. wbfdm/analysis/financial_analysis/statement_with_estimates.py +5 -6
  5. wbfdm/analysis/financial_analysis/utils.py +6 -0
  6. wbfdm/contrib/dsws/client.py +3 -3
  7. wbfdm/contrib/dsws/dataloaders/market_data.py +31 -3
  8. wbfdm/contrib/internal/dataloaders/market_data.py +43 -9
  9. wbfdm/contrib/metric/backends/base.py +2 -2
  10. wbfdm/contrib/metric/backends/statistics.py +47 -13
  11. wbfdm/contrib/metric/dispatch.py +3 -0
  12. wbfdm/contrib/metric/exceptions.py +1 -1
  13. wbfdm/contrib/metric/filters.py +19 -0
  14. wbfdm/contrib/metric/models.py +6 -0
  15. wbfdm/contrib/metric/orchestrators.py +4 -4
  16. wbfdm/contrib/metric/signals.py +7 -0
  17. wbfdm/contrib/metric/tasks.py +2 -3
  18. wbfdm/contrib/metric/viewsets/configs/display.py +2 -2
  19. wbfdm/contrib/metric/viewsets/mixins.py +6 -6
  20. wbfdm/contrib/msci/client.py +6 -2
  21. wbfdm/contrib/qa/database_routers.py +1 -1
  22. wbfdm/contrib/qa/dataloaders/adjustments.py +2 -1
  23. wbfdm/contrib/qa/dataloaders/corporate_actions.py +2 -1
  24. wbfdm/contrib/qa/dataloaders/financials.py +19 -1
  25. wbfdm/contrib/qa/dataloaders/fx_rates.py +86 -0
  26. wbfdm/contrib/qa/dataloaders/market_data.py +29 -40
  27. wbfdm/contrib/qa/dataloaders/officers.py +1 -1
  28. wbfdm/contrib/qa/dataloaders/statements.py +18 -3
  29. wbfdm/contrib/qa/jinja2/qa/sql/ibes/financials.sql +1 -1
  30. wbfdm/contrib/qa/sync/exchanges.py +2 -1
  31. wbfdm/contrib/qa/sync/utils.py +76 -17
  32. wbfdm/dataloaders/protocols.py +12 -1
  33. wbfdm/dataloaders/proxies.py +15 -1
  34. wbfdm/dataloaders/types.py +7 -1
  35. wbfdm/enums.py +2 -0
  36. wbfdm/factories/instruments.py +4 -2
  37. wbfdm/figures/financials/financial_analysis_charts.py +2 -8
  38. wbfdm/filters/classifications.py +2 -2
  39. wbfdm/filters/financials.py +9 -18
  40. wbfdm/filters/financials_analysis.py +36 -16
  41. wbfdm/filters/instrument_prices.py +8 -5
  42. wbfdm/filters/instruments.py +21 -7
  43. wbfdm/import_export/backends/cbinsights/utils/client.py +8 -8
  44. wbfdm/import_export/backends/refinitiv/utils/controller.py +1 -1
  45. wbfdm/import_export/handlers/instrument.py +160 -104
  46. wbfdm/import_export/handlers/option.py +2 -2
  47. wbfdm/import_export/parsers/cbinsights/equities.py +2 -3
  48. wbfdm/jinja2.py +2 -1
  49. wbfdm/locale/de/LC_MESSAGES/django.mo +0 -0
  50. wbfdm/locale/de/LC_MESSAGES/django.po +257 -0
  51. wbfdm/locale/en/LC_MESSAGES/django.mo +0 -0
  52. wbfdm/locale/en/LC_MESSAGES/django.po +255 -0
  53. wbfdm/locale/fr/LC_MESSAGES/django.mo +0 -0
  54. wbfdm/locale/fr/LC_MESSAGES/django.po +257 -0
  55. wbfdm/migrations/0031_exchange_apply_round_lot_size_and_more.py +23 -0
  56. wbfdm/migrations/0032_alter_instrumentprice_outstanding_shares.py +18 -0
  57. wbfdm/migrations/0033_alter_controversy_review.py +18 -0
  58. wbfdm/migrations/0034_alter_instrumentlist_instrument_list_type.py +18 -0
  59. wbfdm/models/esg/controversies.py +19 -23
  60. wbfdm/models/exchanges/exchanges.py +8 -4
  61. wbfdm/models/fields.py +2 -2
  62. wbfdm/models/fk_fields.py +3 -3
  63. wbfdm/models/instruments/instrument_lists.py +1 -0
  64. wbfdm/models/instruments/instrument_prices.py +8 -1
  65. wbfdm/models/instruments/instrument_relationships.py +3 -0
  66. wbfdm/models/instruments/instruments.py +139 -26
  67. wbfdm/models/instruments/llm/create_instrument_news_relationships.py +29 -22
  68. wbfdm/models/instruments/mixin/financials_computed.py +0 -4
  69. wbfdm/models/instruments/mixin/financials_serializer_fields.py +118 -118
  70. wbfdm/models/instruments/mixin/instruments.py +7 -4
  71. wbfdm/models/instruments/options.py +6 -0
  72. wbfdm/models/instruments/private_equities.py +3 -0
  73. wbfdm/models/instruments/querysets.py +138 -37
  74. wbfdm/models/instruments/utils.py +5 -0
  75. wbfdm/serializers/exchanges.py +1 -0
  76. wbfdm/serializers/instruments/__init__.py +1 -0
  77. wbfdm/serializers/instruments/instruments.py +9 -2
  78. wbfdm/serializers/instruments/mixins.py +3 -3
  79. wbfdm/tasks.py +13 -2
  80. wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +0 -1
  81. wbfdm/tests/models/test_instrument_prices.py +0 -14
  82. wbfdm/tests/models/test_instruments.py +21 -9
  83. wbfdm/tests/models/test_queryset.py +89 -0
  84. wbfdm/viewsets/configs/display/exchanges.py +1 -1
  85. wbfdm/viewsets/configs/display/financial_summary.py +2 -2
  86. wbfdm/viewsets/configs/display/instrument_prices.py +2 -70
  87. wbfdm/viewsets/configs/display/instruments.py +3 -4
  88. wbfdm/viewsets/configs/display/instruments_relationships.py +3 -1
  89. wbfdm/viewsets/configs/display/prices.py +1 -0
  90. wbfdm/viewsets/configs/display/statement_with_estimates.py +1 -2
  91. wbfdm/viewsets/configs/endpoints/classifications.py +0 -12
  92. wbfdm/viewsets/configs/endpoints/instrument_prices.py +4 -23
  93. wbfdm/viewsets/configs/titles/instrument_prices.py +2 -1
  94. wbfdm/viewsets/esg.py +2 -2
  95. wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +2 -2
  96. wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +1 -1
  97. wbfdm/viewsets/financial_analysis/financial_summary.py +6 -6
  98. wbfdm/viewsets/financial_analysis/statement_with_estimates.py +7 -3
  99. wbfdm/viewsets/instruments/financials_analysis.py +9 -12
  100. wbfdm/viewsets/instruments/instrument_prices.py +9 -9
  101. wbfdm/viewsets/instruments/instruments.py +9 -7
  102. wbfdm/viewsets/instruments/utils.py +3 -3
  103. wbfdm/viewsets/market_data.py +1 -1
  104. wbfdm/viewsets/prices.py +5 -0
  105. wbfdm/viewsets/statements/statements.py +7 -3
  106. {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/METADATA +2 -1
  107. {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/RECORD +108 -95
  108. {wbfdm-1.49.5.dist-info → wbfdm-1.59.4.dist-info}/WHEEL +1 -1
  109. wbfdm/menu.py +0 -11
@@ -7,6 +7,7 @@ from wbfdm.dataloaders.types import (
7
7
  ESGControversyDataDict,
8
8
  ESGDataDict,
9
9
  FinancialDataDict,
10
+ FXRateDict,
10
11
  MarketDataDict,
11
12
  OfficerDataDict,
12
13
  ReportDateDataDict,
@@ -36,15 +37,25 @@ class AdjustmentsProtocol(Protocol):
36
37
  ) -> Iterator[AdjustmentDataDict]: ...
37
38
 
38
39
 
40
+ class FXRateProtocol(Protocol):
41
+ def fx_rates(
42
+ self,
43
+ from_date: date,
44
+ to_date: date,
45
+ target_currency: str,
46
+ ) -> Iterator[FXRateDict]: ...
47
+
48
+
39
49
  class MarketDataProtocol(Protocol):
40
50
  def market_data(
41
51
  self,
42
- values: list[MarketData] = [MarketData.CLOSE],
52
+ values: list[MarketData] | None = None,
43
53
  from_date: date | None = None,
44
54
  to_date: date | None = None,
45
55
  exact_date: date | None = None,
46
56
  frequency: Frequency = Frequency.DAILY,
47
57
  target_currency: str | None = None,
58
+ apply_fx_rate: bool = True,
48
59
  ) -> Iterator[MarketDataDict]: ...
49
60
 
50
61
 
@@ -9,6 +9,7 @@ from wbfdm.dataloaders.protocols import (
9
9
  ESGControversyProtocol,
10
10
  ESGProtocol,
11
11
  FinancialsProtocol,
12
+ FXRateProtocol,
12
13
  MarketDataProtocol,
13
14
  OfficersProtocol,
14
15
  ReportDateProtocol,
@@ -20,6 +21,7 @@ from wbfdm.dataloaders.types import (
20
21
  ESGControversyDataDict,
21
22
  ESGDataDict,
22
23
  FinancialDataDict,
24
+ FXRateDict,
23
25
  MarketDataDict,
24
26
  OfficerDataDict,
25
27
  ReportDateDataDict,
@@ -53,7 +55,8 @@ def _market_data_row_parser(row):
53
55
 
54
56
  class InstrumentDataloaderProxy(
55
57
  DataloaderProxy[
56
- AdjustmentsProtocol
58
+ FXRateProtocol
59
+ | AdjustmentsProtocol
57
60
  | MarketDataProtocol
58
61
  | CorporateActionsProtocol
59
62
  | OfficersProtocol
@@ -68,6 +71,15 @@ class InstrumentDataloaderProxy(
68
71
  for dl in self.iterate_dataloaders("reporting_dates"):
69
72
  yield from dl.reporting_dates(only_next=only_next)
70
73
 
74
+ def fx_rates(
75
+ self,
76
+ from_date: date,
77
+ to_date: date,
78
+ target_currency: str,
79
+ ) -> Iterator[FXRateDict]:
80
+ for dl in self.iterate_dataloaders("fx_rates"):
81
+ yield from dl.fx_rates(from_date, to_date, target_currency)
82
+
71
83
  def adjustments(self, from_date: date | None = None, to_date: date | None = None) -> Iterator[AdjustmentDataDict]:
72
84
  for dl in self.iterate_dataloaders("adjustments"):
73
85
  yield from dl.adjustments(from_date=from_date, to_date=to_date)
@@ -80,6 +92,7 @@ class InstrumentDataloaderProxy(
80
92
  exact_date: date | None = None,
81
93
  frequency: Frequency = Frequency.DAILY,
82
94
  target_currency: str | None = None,
95
+ apply_fx_rate: bool = True,
83
96
  ) -> Iterator[MarketDataDict]:
84
97
  if not values:
85
98
  values = list(MarketData)
@@ -93,6 +106,7 @@ class InstrumentDataloaderProxy(
93
106
  exact_date=exact_date,
94
107
  frequency=frequency,
95
108
  target_currency=target_currency,
109
+ apply_fx_rate=apply_fx_rate,
96
110
  ),
97
111
  )
98
112
 
@@ -26,6 +26,12 @@ class BaseDict(TypedDict):
26
26
  currency: NotRequired[str]
27
27
 
28
28
 
29
+ class FXRateDict(TypedDict):
30
+ currency_pair: str
31
+ fx_date: date
32
+ fx_rate: float
33
+
34
+
29
35
  class MarketDataDict(BaseDict):
30
36
  """
31
37
  Represents a dictionary for daily valuation data.
@@ -65,7 +71,7 @@ class MarketDataDict(BaseDict):
65
71
  volume: NotRequired[float]
66
72
  market_cap: NotRequired[float]
67
73
  calculated: NotRequired[bool]
68
-
74
+ fx_rate: NotRequired[float]
69
75
  unadjusted_close: NotRequired[float]
70
76
  unadjusted_outstanding_shares: NotRequired[float | int]
71
77
 
wbfdm/enums.py CHANGED
@@ -21,12 +21,14 @@ class MarketData(ChoiceEnum):
21
21
  VOLUME = "volume"
22
22
  SHARES_OUTSTANDING = "outstanding_shares"
23
23
  MARKET_CAPITALIZATION = "market_capitalization"
24
+ MARKET_CAPITALIZATION_CONSOLIDATED = "market_capitalization_consolidated"
24
25
 
25
26
  @classmethod
26
27
  def name_mapping(cls):
27
28
  return {
28
29
  cls.CLOSE.value: "Closing Price",
29
30
  cls.MARKET_CAPITALIZATION.value: "Market Cap.",
31
+ cls.MARKET_CAPITALIZATION_CONSOLIDATED.value: "Market Cap. (Consolidated)",
30
32
  }
31
33
 
32
34
 
@@ -21,13 +21,15 @@ class InstrumentFactory(factory.django.DjangoModelFactory):
21
21
  instrument_type = factory.SubFactory(InstrumentTypeFactory)
22
22
  inception_date = factory.Faker("past_date")
23
23
  delisted_date = None
24
- currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyFactory")
24
+ currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyUSDFactory")
25
25
  country = factory.SubFactory("wbcore.contrib.geography.factories.CountryFactory")
26
26
  exchange = factory.SubFactory("wbfdm.factories.exchanges.ExchangeFactory")
27
27
 
28
28
  identifier = factory.LazyAttribute(lambda o: rstr.xeger("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})"))
29
29
  isin = factory.LazyAttribute(lambda o: rstr.xeger("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})"))
30
30
  ticker = factory.LazyAttribute(lambda o: rstr.xeger("([A-Z]{4})"))
31
+ refinitiv_identifier_code = factory.LazyAttribute(lambda o: rstr.xeger("([A-Z]{4})"))
32
+ sedol = factory.LazyAttribute(lambda o: rstr.xeger("([A-Z]{4})"))
31
33
  dl_parameters = {"market_data": {"path": "wbfdm.contrib.internal.dataloaders.market_data.MarketDataDataloader"}}
32
34
 
33
35
  @factory.post_generation
@@ -54,7 +56,7 @@ class InstrumentFactory(factory.django.DjangoModelFactory):
54
56
 
55
57
  class CashFactory(factory.django.DjangoModelFactory):
56
58
  is_cash = True
57
- currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyFactory")
59
+ currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyUSDFactory")
58
60
  name = factory.LazyAttribute(lambda o: o.currency.title)
59
61
  instrument_type = factory.LazyAttribute(
60
62
  lambda o: InstrumentType.objects.get_or_create(key="cash", defaults={"name": "Cash", "short_name": "Cash"})[0]
@@ -115,13 +115,7 @@ class FinancialAnalysisGenerator:
115
115
  self.instruments_repr_map = {i.id: i.name_repr for i in self.instruments}
116
116
  self.currency_map = {i.id: i.currency.id for i in self.instruments}
117
117
 
118
- def build_df(
119
- self,
120
- instrument_prices_field_names: list[str] = [],
121
- fundamental_field_names: list[str] = [],
122
- forecast_field_names: list[str] = [],
123
- daily_fundamental_field_names: list[str] = [],
124
- ):
118
+ def build_df(self, **kwargs):
125
119
  """
126
120
  Used to returns a df with all the variables passed in four separate lists
127
121
 
@@ -239,7 +233,7 @@ class FinancialAnalysisGenerator:
239
233
  @staticmethod
240
234
  def clean_data(
241
235
  df: pd.DataFrame,
242
- var_list: list[str] = [],
236
+ var_list: list[str],
243
237
  drop_negative=True,
244
238
  q_low: float = 0.05,
245
239
  q_high: float = 0.95,
@@ -67,7 +67,7 @@ class ClassificationTreeChartFilter(wb_filters.FilterSet):
67
67
 
68
68
  aggregation_type = wb_filters.ChoiceFilter(
69
69
  choices=[("classification_count", "Classification Count"), ("instrument_count", "Instrument Count")],
70
- default="classification_count",
70
+ initial="classification_count",
71
71
  label="Aggregation Type",
72
72
  method="fake_filter",
73
73
  )
@@ -89,7 +89,7 @@ class InstrumentClassificationThroughModelViewFilterSet(wb_filters.FilterSet):
89
89
  endpoint=ClassificationGroup.get_representation_endpoint(),
90
90
  value_key=ClassificationGroup.get_representation_value_key(),
91
91
  label_key=ClassificationGroup.get_representation_label_key(),
92
- default=_get_default_classification_group_id,
92
+ initial=_get_default_classification_group_id,
93
93
  )
94
94
 
95
95
  class Meta:
@@ -7,15 +7,14 @@ from wbfdm.models.instruments import Instrument
7
7
 
8
8
  class MarketDataChartFilterSet(filters.FilterSet):
9
9
  period = filters.FinancialPerformanceDateRangeFilter(
10
- label="Period",
11
- method="fake_filter",
12
- default=five_year_data_range,
10
+ label="Period", method="fake_filter", initial=five_year_data_range, required=True
13
11
  )
14
12
  chart_type = filters.ChoiceFilter(
15
13
  method="fake_filter",
16
14
  label="Chart Type",
17
15
  choices=MarketDataChartType.choices,
18
- default="close",
16
+ required=True,
17
+ initial=MarketDataChartType.CLOSE,
19
18
  )
20
19
  benchmarks = filters.ModelMultipleChoiceFilter(
21
20
  label="Benchmarks",
@@ -35,14 +34,12 @@ class MarketDataChartFilterSet(filters.FilterSet):
35
34
  volume = filters.BooleanFilter(
36
35
  method="fake_filter",
37
36
  label="Add Volume",
38
- default=False,
39
- required=False,
37
+ initial=False,
40
38
  )
41
39
  show_estimates = filters.BooleanFilter(
42
40
  method="fake_filter",
43
41
  label="Show Estimates",
44
- default=True,
45
- required=False,
42
+ initial=True,
46
43
  )
47
44
 
48
45
  class Meta:
@@ -51,16 +48,10 @@ class MarketDataChartFilterSet(filters.FilterSet):
51
48
 
52
49
 
53
50
  class FinancialRatioFilterSet(filters.FilterSet):
54
- ttm = filters.BooleanFilter(
55
- method="fake_filter",
56
- label="TTM/FTM",
57
- default=True,
58
- )
51
+ ttm = filters.BooleanFilter(method="fake_filter", label="TTM/FTM", initial=True, required=True)
59
52
 
60
53
  period = filters.FinancialPerformanceDateRangeFilter(
61
- method="fake_filter",
62
- label="Period",
63
- default=five_year_data_range,
54
+ method="fake_filter", label="Period", initial=five_year_data_range, required=True
64
55
  )
65
56
 
66
57
  class Meta:
@@ -74,7 +65,7 @@ class StatementFilter(filters.FilterSet):
74
65
  label="Data Type",
75
66
  choices=DataType.choices,
76
67
  required=True,
77
- default=DataType.STANDARDIZED,
68
+ initial=DataType.STANDARDIZED,
78
69
  )
79
70
 
80
71
  class Meta:
@@ -88,7 +79,7 @@ class StatementWithEstimateFilter(filters.FilterSet):
88
79
  label="Calendar Type",
89
80
  choices=CalendarType.choices,
90
81
  required=True,
91
- default=CalendarType.FISCAL,
82
+ initial=CalendarType.FISCAL,
92
83
  )
93
84
 
94
85
  class Meta:
@@ -48,7 +48,7 @@ class FinancialAnalysisValuationRatiosFilterSet(wb_filters.FilterSet):
48
48
  label="Date Range",
49
49
  required=True,
50
50
  clearable=False,
51
- default=lambda r, v, q: DateRange(_get_12m(r, v, q), date.today()),
51
+ initial=lambda r, v, q: DateRange(_get_12m(r, v, q), date.today()),
52
52
  )
53
53
 
54
54
  class OutputChoices(models.TextChoices):
@@ -61,34 +61,46 @@ class FinancialAnalysisValuationRatiosFilterSet(wb_filters.FilterSet):
61
61
  ROLLING = "ROLLING", "Rolling"
62
62
 
63
63
  output = wb_filters.ChoiceFilter(
64
- choices=OutputChoices.choices, label="Output", method="fake_filter", default=OutputChoices.CHART
64
+ choices=OutputChoices.choices, label="Output", method="fake_filter", initial=OutputChoices.CHART, required=True
65
65
  )
66
66
  period = wb_filters.ChoiceFilter(
67
- choices=PeriodChoices.choices, label="Period", method="fake_filter", default=PeriodChoices.NTM
67
+ choices=PeriodChoices.choices, label="Period", method="fake_filter", initial=PeriodChoices.NTM, required=True
68
68
  )
69
- vs_related = wb_filters.BooleanFilter(label="Versus related", default=False, method="fake_filter")
70
- clean_data = wb_filters.BooleanFilter(label="Clean data", default=True, method="fake_filter")
71
- ranges = wb_filters.BooleanFilter(label="Draw ranges", default=False, required=False, method="fake_filter")
69
+ vs_related = wb_filters.BooleanFilter(label="Versus related", initial=False, required=True, method="fake_filter")
70
+ clean_data = wb_filters.BooleanFilter(label="Clean data", initial=True, required=True, method="fake_filter")
71
+ ranges = wb_filters.BooleanFilter(label="Draw ranges", initial=False, method="fake_filter")
72
72
  range_type = wb_filters.ChoiceFilter(
73
73
  choices=RangeChoices.choices,
74
74
  label="Range type",
75
75
  method="fake_filter",
76
76
  required=True,
77
- default=RangeChoices.MINMAX,
77
+ initial=RangeChoices.MINMAX,
78
78
  )
79
79
  range_period = wb_filters.NumberFilter(
80
- precision=0, label="Rolling period", method="fake_filter", required=True, default=120
80
+ precision=0, label="Rolling period", method="fake_filter", required=True, initial=120
81
81
  )
82
82
  x_axis_var = wb_filters.ChoiceFilter(
83
- choices=VariableChoices.choices, label="X-Axis", method="fake_filter", default=VariableChoices.EPSG
83
+ choices=VariableChoices.choices,
84
+ label="X-Axis",
85
+ method="fake_filter",
86
+ initial=VariableChoices.EPSG,
87
+ required=True,
84
88
  )
85
89
  y_axis_var = wb_filters.ChoiceFilter(
86
- choices=VariableChoices.choices, label="Y-Axis", method="fake_filter", default=VariableChoices.PE
90
+ choices=VariableChoices.choices,
91
+ label="Y-Axis",
92
+ method="fake_filter",
93
+ initial=VariableChoices.PE,
94
+ required=True,
87
95
  )
88
96
  z_axis_var = wb_filters.ChoiceFilter(
89
- choices=VariableChoices.choices, label="Bubble", method="fake_filter", default=VariableChoices.MKTCAP
97
+ choices=VariableChoices.choices,
98
+ label="Bubble",
99
+ method="fake_filter",
100
+ initial=VariableChoices.MKTCAP,
101
+ required=True,
90
102
  )
91
- median = wb_filters.BooleanFilter(label="Median", default=True, method="fake_filter")
103
+ median = wb_filters.BooleanFilter(label="Median", initial=True, required=True, method="fake_filter")
92
104
 
93
105
  class Meta:
94
106
  model = Instrument
@@ -101,19 +113,27 @@ class EarningsAnalysisFilterSet(wb_filters.FilterSet):
101
113
  label="Date Range",
102
114
  required=True,
103
115
  clearable=False,
104
- default=lambda r, v, q: DateRange(_get_12m(r, v, q), date.today()),
116
+ initial=lambda r, v, q: DateRange(_get_12m(r, v, q), date.today()),
105
117
  )
106
118
 
107
119
  class OutputChoices(models.TextChoices):
108
120
  EPS = "EPS", "Earnings ($)"
109
121
 
110
122
  analysis = wb_filters.ChoiceFilter(
111
- choices=OutputChoices.choices, label="Analysis", method=lambda q, n, v: q, default=OutputChoices.EPS
123
+ choices=OutputChoices.choices,
124
+ label="Analysis",
125
+ method=lambda q, n, v: q,
126
+ initial=OutputChoices.EPS,
127
+ required=True,
112
128
  )
113
129
  period = wb_filters.ChoiceFilter(
114
- choices=PeriodChoices.choices, label="Period", method=lambda q, n, v: q, default=PeriodChoices.NTM
130
+ choices=PeriodChoices.choices,
131
+ label="Period",
132
+ method=lambda q, n, v: q,
133
+ initial=PeriodChoices.NTM,
134
+ required=True,
115
135
  )
116
- vs_related = wb_filters.BooleanFilter(label="Show related", default=False, method=lambda q, n, v: q)
136
+ vs_related = wb_filters.BooleanFilter(label="Show related", initial=False, required=True, method=lambda q, n, v: q)
117
137
 
118
138
  class Meta:
119
139
  model = Instrument
@@ -16,7 +16,7 @@ class FakeDateRange(wb_filters.FilterSet):
16
16
  label="Date Range",
17
17
  required=True,
18
18
  clearable=False,
19
- default=lambda r, v, q: DateRange(byearend_2_year_ago(r, v, q), date.today()),
19
+ initial=lambda r, v, q: DateRange(byearend_2_year_ago(r, v, q), date.today()),
20
20
  )
21
21
 
22
22
  class Meta:
@@ -26,11 +26,10 @@ class FakeDateRange(wb_filters.FilterSet):
26
26
 
27
27
  class InstrumentPriceFilterSet(wb_filters.FilterSet):
28
28
  date = wb_filters.FinancialPerformanceDateRangeFilter(
29
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
30
29
  label="Date Range",
31
30
  required=True,
32
31
  clearable=False,
33
- default=lambda r, v, q: DateRange(get_earliest_date(r, v, q), get_latest_date(r, v, q)),
32
+ initial=lambda r, v, q: DateRange(get_earliest_date(r, v, q), get_latest_date(r, v, q)),
34
33
  )
35
34
 
36
35
  class Meta:
@@ -70,7 +69,7 @@ class InstrumentPriceMultipleBenchmarkChartFilterSet(InstrumentPriceFilterSet):
70
69
  filter_params={"is_security": True},
71
70
  method="fake_filter",
72
71
  )
73
- normalized = wb_filters.BooleanFilter(label="Normalize", default=True, method="fake_filter")
72
+ normalized = wb_filters.BooleanFilter(label="Normalize", initial=True, required=True, method="fake_filter")
74
73
 
75
74
  class Meta:
76
75
  model = InstrumentPrice
@@ -84,7 +83,11 @@ class InstrumentPriceFrequencyFilter(InstrumentPriceFilterSet):
84
83
  MONTHLY = "BME", "Monthly"
85
84
 
86
85
  frequency = wb_filters.ChoiceFilter(
87
- label="Frequency", choices=FrequencyChoice.choices, default=FrequencyChoice.DAILY, method="fake_filter"
86
+ label="Frequency",
87
+ choices=FrequencyChoice.choices,
88
+ initial=FrequencyChoice.DAILY,
89
+ required=True,
90
+ method="fake_filter",
88
91
  )
89
92
 
90
93
  class Meta:
@@ -29,7 +29,7 @@ class InstrumentFavoriteGroupFilterSet(wb_filters.FilterSet):
29
29
  endpoint=InstrumentFavoriteGroup.get_representation_endpoint(),
30
30
  value_key=InstrumentFavoriteGroup.get_representation_value_key(),
31
31
  label_key=InstrumentFavoriteGroup.get_representation_label_key(),
32
- default=get_default_favorite_group,
32
+ initial=get_default_favorite_group,
33
33
  method="filter_favorite_group",
34
34
  )
35
35
 
@@ -44,6 +44,21 @@ class InstrumentFavoriteGroupFilterSet(wb_filters.FilterSet):
44
44
 
45
45
 
46
46
  class InstrumentFilterSet(TagFilterMixin, InstrumentFavoriteGroupFilterSet):
47
+ sibling_of = wb_filters.ModelChoiceFilter(
48
+ label="Sibling Of",
49
+ queryset=Instrument.objects.all(),
50
+ endpoint=Instrument.get_representation_endpoint(),
51
+ value_key=Instrument.get_representation_value_key(),
52
+ label_key=Instrument.get_representation_label_key(),
53
+ filter_params={"is_investable_universe": True},
54
+ method="filter_sibling_of",
55
+ )
56
+
57
+ def filter_sibling_of(self, queryset, name, value):
58
+ if value:
59
+ return queryset.filter(currency=value.currency, parent=value.parent).exclude(id=value.id)
60
+ return queryset
61
+
47
62
  parent = wb_filters.ModelChoiceFilter(
48
63
  label="Parent",
49
64
  queryset=Instrument.objects.all(),
@@ -52,6 +67,7 @@ class InstrumentFilterSet(TagFilterMixin, InstrumentFavoriteGroupFilterSet):
52
67
  label_key=Instrument.get_representation_label_key(),
53
68
  hidden=True,
54
69
  )
70
+ parent__isnull = wb_filters.BooleanFilter(field_name="parent", lookup_expr="isnull", hidden=True)
55
71
 
56
72
  classifications = wb_filters.ModelChoiceFilter(
57
73
  label="Classification",
@@ -92,9 +108,7 @@ class InstrumentFilterSet(TagFilterMixin, InstrumentFavoriteGroupFilterSet):
92
108
  )
93
109
  return queryset
94
110
 
95
- is_investable = wb_filters.BooleanFilter(
96
- label="Is Investable", default=True, method="filter_is_investable", hidden=True
97
- )
111
+ is_investable = wb_filters.BooleanFilter(label="Is Investable", method="filter_is_investable", hidden=True)
98
112
 
99
113
  def filter_is_investable(self, queryset, name, value):
100
114
  if value:
@@ -139,7 +153,7 @@ class InstrumentFilterSet(TagFilterMixin, InstrumentFavoriteGroupFilterSet):
139
153
  if "parent" in data:
140
154
  data.pop("classifications", None) # remove classifications in case we are navigating the tree
141
155
  data.pop("level", None)
142
- super().__init__(data=data, *args, **kwargs)
156
+ super().__init__(*args, data=data, **kwargs)
143
157
 
144
158
  class Meta:
145
159
  model = Instrument
@@ -171,7 +185,7 @@ class BaseClassifiedInstrumentFilterSet(TagFilterMixin, wb_filters.FilterSet):
171
185
  endpoint=ClassificationGroup.get_representation_endpoint(),
172
186
  value_key=ClassificationGroup.get_representation_value_key(),
173
187
  label_key=ClassificationGroup.get_representation_label_key(),
174
- default=_get_default_classification_group_id,
188
+ initial=_get_default_classification_group_id,
175
189
  method="fake_filter",
176
190
  required=True,
177
191
  )
@@ -195,7 +209,7 @@ class MonthlyPerformancesInstrumentFilterSet(wb_filters.FilterSet):
195
209
  required=True,
196
210
  clearable=False,
197
211
  method="fake_filter",
198
- default=lambda r, v, q: DateRange(get_earliest_date(r, v, q), get_latest_date(r, v, q)),
212
+ initial=lambda r, v, q: DateRange(get_earliest_date(r, v, q), get_latest_date(r, v, q)),
199
213
  )
200
214
 
201
215
  class Meta:
@@ -7,11 +7,11 @@ from celery import shared_task
7
7
  from tqdm import tqdm
8
8
 
9
9
 
10
- class RateLimitException(Exception):
10
+ class RateLimitError(Exception):
11
11
  pass
12
12
 
13
13
 
14
- class CreditLimitException(Exception):
14
+ class CreditLimitError(Exception):
15
15
  pass
16
16
 
17
17
 
@@ -38,17 +38,18 @@ class Client:
38
38
  url,
39
39
  params={**params, **{"nextPageToken": next_page_token}} if next_page_token else params,
40
40
  headers={"authorization": self.jwt_header_value},
41
+ timeout=10,
41
42
  )
42
43
  if resp.status_code != 200:
43
44
  if resp.status_code == 429:
44
- raise RateLimitException()
45
+ raise RateLimitError()
45
46
  raise requests.ConnectionError(
46
47
  f"unexpected error from api\nstatus_code: {resp.status_code}\nerror: {resp.text}"
47
48
  )
48
49
 
49
50
  if credits_remaining_str := resp.headers.get("x-cbinsights-credits-remaining", None):
50
51
  if int(credits_remaining_str) <= 0:
51
- raise CreditLimitException()
52
+ raise CreditLimitError()
52
53
 
53
54
  return resp.json()
54
55
 
@@ -72,11 +73,11 @@ class Client:
72
73
  resp = self._request(data_url, params=params, next_page_token=next_page_token)
73
74
  yield resp
74
75
  next_page_token = resp["nextPageToken"]
75
- except RateLimitException:
76
+ except RateLimitError:
76
77
  time.sleep(self.rate_limit_sleep)
77
78
  retry += 1
78
79
  if retry >= 5:
79
- raise RateLimitException()
80
+ raise RateLimitError()
80
81
 
81
82
  def _chunk_paginated_request(self, data_url, org_ids, extra_params, endpoint, debug: bool = False):
82
83
  data = []
@@ -96,8 +97,7 @@ class Client:
96
97
  auth_url = "https://api.cbinsights.com/v1/authorize"
97
98
 
98
99
  auth_resp = requests.get(
99
- auth_url,
100
- params={"clientId": self.client_id, "clientSecret": self.client_secret},
100
+ auth_url, params={"clientId": self.client_id, "clientSecret": self.client_secret}, timeout=10
101
101
  )
102
102
 
103
103
  if auth_resp.status_code != 200:
@@ -61,7 +61,7 @@ class Controller:
61
61
  instrument_ric: str = None,
62
62
  instrument_isin: str = None,
63
63
  instrument_mnemonic: str = None,
64
- perm_id_symbols: list[str] = ["QPID", "IPID"],
64
+ perm_id_symbols: tuple[str, ...] = ("QPID", "IPID"),
65
65
  ) -> str | None:
66
66
  def _process_ticker(ticker):
67
67
  if not (df := self.client.get_static_df(tickers=[ticker], fields=perm_id_symbols)).empty: