wbportfolio 1.52.0__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.
Potentially problematic release.
This version of wbportfolio might be problematic. Click here for more details.
- wbportfolio/admin/__init__.py +3 -1
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +16 -0
- wbportfolio/admin/orders/orders.py +32 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -2
- wbportfolio/admin/transactions/dividends.py +40 -4
- wbportfolio/admin/transactions/fees.py +24 -14
- wbportfolio/admin/transactions/trades.py +34 -27
- wbportfolio/analysis/claims.py +5 -6
- wbportfolio/api_clients/ubs.py +162 -0
- wbportfolio/constants.py +1 -0
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/defaults/fees/default.py +7 -15
- wbportfolio/factories/__init__.py +2 -2
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/dividends.py +8 -3
- wbportfolio/factories/fees.py +8 -4
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +21 -0
- wbportfolio/factories/orders/orders.py +34 -0
- wbportfolio/factories/portfolios.py +2 -1
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +12 -16
- wbportfolio/filters/assets.py +18 -4
- wbportfolio/filters/orders/__init__.py +2 -0
- wbportfolio/filters/orders/order_proposals.py +55 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/filters/portfolios.py +38 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/__init__.py +1 -2
- wbportfolio/filters/transactions/fees.py +5 -12
- wbportfolio/filters/transactions/trades.py +16 -8
- wbportfolio/filters/transactions/utils.py +42 -0
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +22 -10
- wbportfolio/import_export/handlers/dividend.py +8 -8
- wbportfolio/import_export/handlers/fees.py +13 -23
- wbportfolio/import_export/handlers/orders.py +71 -0
- wbportfolio/import_export/handlers/trade.py +53 -77
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
- wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
- wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
- wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/fees.py +7 -9
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
- wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
- wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
- wbportfolio/import_export/resources/trades.py +3 -3
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
- wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
- wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
- wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
- wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
- wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +28 -170
- wbportfolio/models/builder.py +323 -0
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/orders/order_proposals.py +1414 -0
- wbportfolio/models/orders/orders.py +410 -0
- wbportfolio/models/portfolio.py +311 -289
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +12 -0
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/__init__.py +0 -4
- wbportfolio/models/transactions/claim.py +7 -6
- wbportfolio/models/transactions/dividends.py +42 -5
- wbportfolio/models/transactions/fees.py +55 -22
- wbportfolio/models/transactions/trades.py +121 -442
- wbportfolio/models/transactions/transactions.py +78 -158
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +35 -0
- wbportfolio/order_routing/adapters/__init__.py +65 -0
- wbportfolio/order_routing/adapters/ubs.py +195 -0
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/__init__.py +0 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/analytics/portfolio.py +17 -9
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +198 -63
- wbportfolio/rebalancing/base.py +12 -1
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +4 -8
- wbportfolio/rebalancing/models/equally_weighted.py +13 -11
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
- wbportfolio/rebalancing/models/model_portfolio.py +14 -18
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/orders/order_proposals.py +115 -0
- wbportfolio/serializers/orders/orders.py +283 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/signals.py +9 -12
- wbportfolio/serializers/transactions/__init__.py +1 -10
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/serializers/transactions/dividends.py +37 -9
- wbportfolio/serializers/transactions/fees.py +39 -10
- wbportfolio/serializers/transactions/trades.py +55 -157
- wbportfolio/tasks.py +43 -5
- wbportfolio/tests/analysis/__init__.py +0 -0
- wbportfolio/tests/analysis/test_claims.py +85 -0
- wbportfolio/tests/conftest.py +12 -12
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
- wbportfolio/tests/models/test_assets.py +7 -3
- wbportfolio/tests/models/test_imports.py +9 -13
- wbportfolio/tests/models/test_portfolios.py +102 -95
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_fees.py +7 -13
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/pms/test_analytics.py +22 -3
- wbportfolio/tests/rebalancing/test_models.py +51 -57
- wbportfolio/tests/signals.py +10 -20
- wbportfolio/tests/tests.py +3 -1
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +10 -13
- wbportfolio/viewsets/__init__.py +9 -4
- wbportfolio/viewsets/assets.py +3 -204
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +344 -154
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/__init__.py +2 -5
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/fees.py +3 -3
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/configs/display/trades.py +1 -189
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
- wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/configs/menu/__init__.py +1 -1
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/configs/titles/__init__.py +2 -3
- wbportfolio/viewsets/configs/titles/fees.py +4 -8
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/mixins.py +5 -1
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
- wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
- wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/orders/order_proposals.py +252 -0
- wbportfolio/viewsets/orders/orders.py +277 -0
- wbportfolio/viewsets/portfolios.py +36 -12
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/products.py +6 -6
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +3 -14
- wbportfolio/viewsets/transactions/fees.py +22 -22
- wbportfolio/viewsets/transactions/trades.py +1 -180
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/admin/transactions/transactions.py +0 -38
- wbportfolio/factories/transactions.py +0 -22
- wbportfolio/fdm/tasks.py +0 -13
- wbportfolio/filters/transactions/transactions.py +0 -99
- wbportfolio/models/transactions/expiry.py +0 -7
- wbportfolio/models/transactions/trade_proposals.py +0 -704
- wbportfolio/pms/trading/handler.py +0 -161
- wbportfolio/serializers/transactions/expiry.py +0 -18
- wbportfolio/serializers/transactions/trade_proposals.py +0 -76
- wbportfolio/serializers/transactions/transactions.py +0 -85
- wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
- wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
- wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
- wbportfolio/viewsets/configs/display/transactions.py +0 -55
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
- wbportfolio/viewsets/configs/menu/transactions.py +0 -9
- wbportfolio/viewsets/configs/titles/transactions.py +0 -9
- wbportfolio/viewsets/signals.py +0 -43
- wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
- wbportfolio/viewsets/transactions/transactions.py +0 -122
- /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
wbportfolio/factories/trades.py
CHANGED
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
import random
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from decimal import Decimal
|
|
2
4
|
|
|
3
5
|
import factory
|
|
4
6
|
from faker import Faker
|
|
5
|
-
from pandas._libs.tslibs.offsets import BDay
|
|
6
7
|
|
|
7
|
-
from wbportfolio.models import Trade
|
|
8
|
-
|
|
9
|
-
from .transactions import TransactionFactory
|
|
8
|
+
from wbportfolio.models import Trade
|
|
10
9
|
|
|
11
10
|
fake = Faker()
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
class TradeFactory(
|
|
13
|
+
class TradeFactory(factory.django.DjangoModelFactory):
|
|
15
14
|
class Meta:
|
|
16
15
|
model = Trade
|
|
17
16
|
|
|
17
|
+
currency_fx_rate = Decimal(1.0)
|
|
18
|
+
fees = Decimal(0.0)
|
|
19
|
+
portfolio = factory.SubFactory("wbportfolio.factories.PortfolioFactory")
|
|
20
|
+
underlying_instrument = factory.SubFactory("wbfdm.factories.InstrumentFactory")
|
|
21
|
+
currency = factory.SubFactory("wbcore.contrib.currency.factories.CurrencyFactory")
|
|
22
|
+
transaction_date = factory.Faker("date_object")
|
|
23
|
+
value_date = factory.LazyAttribute(lambda o: o.transaction_date + timedelta(days=1))
|
|
18
24
|
bank = factory.Faker("company")
|
|
19
25
|
marked_for_deletion = False
|
|
20
26
|
shares = factory.Faker("pydecimal", min_value=10, max_value=1000, right_digits=4)
|
|
21
27
|
price = factory.LazyAttribute(lambda o: random.randint(10, 10000))
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TradeProposalFactory(factory.django.DjangoModelFactory):
|
|
26
|
-
class Meta:
|
|
27
|
-
model = TradeProposal
|
|
28
|
-
|
|
29
|
-
trade_date = factory.LazyAttribute(lambda o: (fake.date_object() + BDay(1)).date())
|
|
30
|
-
comment = factory.Faker("paragraph")
|
|
31
|
-
portfolio = factory.SubFactory("wbportfolio.factories.PortfolioFactory")
|
|
32
|
-
creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
|
|
28
|
+
# order_proposal = factory.SubFactory("wbportfolio.factories.OrderProposalFactory")
|
|
33
29
|
|
|
34
30
|
|
|
35
31
|
class CustomerTradeFactory(TradeFactory):
|
wbportfolio/filters/assets.py
CHANGED
|
@@ -127,7 +127,7 @@ class AssetPositionFilter(wb_filters.FilterSet):
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
underlying_instrument__is_cash = wb_filters.BooleanFilter(
|
|
130
|
-
label="
|
|
130
|
+
label="Cash", lookup_expr="exact", method="filter_is_cash"
|
|
131
131
|
)
|
|
132
132
|
underlying_instrument__instrument_type = wb_filters.ModelChoiceFilter(
|
|
133
133
|
label="Instrument Type",
|
|
@@ -230,6 +230,13 @@ class AssetPositionFilter(wb_filters.FilterSet):
|
|
|
230
230
|
return queryset.filter(underlying_instrument__in=value.get_classified_instruments())
|
|
231
231
|
return queryset
|
|
232
232
|
|
|
233
|
+
def filter_is_cash(self, queryset, name, value):
|
|
234
|
+
if value is True:
|
|
235
|
+
return queryset.filter(Q(underlying_quote__is_cash=True) | Q(underlying_quote__is_cash_equivalent=True))
|
|
236
|
+
if value is False:
|
|
237
|
+
return queryset.filter(Q(underlying_quote__is_cash=False) & Q(underlying_quote__is_cash_equivalent=False))
|
|
238
|
+
return queryset
|
|
239
|
+
|
|
233
240
|
def filter_portfolio_instrument(self, queryset, name, value):
|
|
234
241
|
if value:
|
|
235
242
|
return queryset.filter(
|
|
@@ -400,8 +407,8 @@ class DistributionFilter(wb_filters.FilterSet):
|
|
|
400
407
|
|
|
401
408
|
group_by = wb_filters.ChoiceFilter(
|
|
402
409
|
label="Group By",
|
|
403
|
-
choices=AssetPositionGroupBy.choices
|
|
404
|
-
initial=AssetPositionGroupBy.INDUSTRY.
|
|
410
|
+
choices=AssetPositionGroupBy.choices,
|
|
411
|
+
initial=AssetPositionGroupBy.INDUSTRY.value,
|
|
405
412
|
method="fake_filter",
|
|
406
413
|
clearable=False,
|
|
407
414
|
required=True,
|
|
@@ -427,6 +434,14 @@ class DistributionFilter(wb_filters.FilterSet):
|
|
|
427
434
|
endpoint=ClassificationGroup.get_representation_endpoint(),
|
|
428
435
|
value_key=ClassificationGroup.get_representation_value_key(),
|
|
429
436
|
label_key=ClassificationGroup.get_representation_label_key(),
|
|
437
|
+
depends_on=[{"field": "group_by", "options": {"activates_on": [AssetPositionGroupBy.INDUSTRY.value]}}],
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
group_by_classification_height = wb_filters.NumberFilter(
|
|
441
|
+
method="fake_filter",
|
|
442
|
+
label="Classification Height",
|
|
443
|
+
initial=0,
|
|
444
|
+
depends_on=[{"field": "group_by", "options": {"activates_on": [AssetPositionGroupBy.INDUSTRY.value]}}],
|
|
430
445
|
)
|
|
431
446
|
|
|
432
447
|
class Meta:
|
|
@@ -441,7 +456,6 @@ def get_default_hedged_currency(field, request, view):
|
|
|
441
456
|
|
|
442
457
|
class ContributionChartFilter(wb_filters.FilterSet):
|
|
443
458
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
444
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
445
459
|
label="Date Range",
|
|
446
460
|
required=True,
|
|
447
461
|
clearable=False,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from django.db.models import Exists, OuterRef
|
|
2
|
+
from wbcore import filters as wb_filters
|
|
3
|
+
|
|
4
|
+
from wbportfolio.models import InstrumentPortfolioThroughModel, OrderProposal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OrderProposalFilterSet(wb_filters.FilterSet):
|
|
8
|
+
has_custodian_adapter = wb_filters.BooleanFilter(
|
|
9
|
+
method="filter_has_custodian_adapter", label="Has Custodian Adapter"
|
|
10
|
+
)
|
|
11
|
+
waiting_for_input = wb_filters.BooleanFilter(method="filter_waiting_for_input", label="Waiting for Input")
|
|
12
|
+
is_automatic_rebalancing = wb_filters.BooleanFilter(
|
|
13
|
+
method="filter_is_automatic_rebalancing", label="Automatic Rebalancing"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def filter_has_custodian_adapter(self, queryset, name, value):
|
|
17
|
+
queryset = queryset.annotate(
|
|
18
|
+
has_custodian_adapter=Exists(
|
|
19
|
+
InstrumentPortfolioThroughModel.objects.filter(
|
|
20
|
+
portfolio=OuterRef("portfolio"), instrument__net_asset_value_computation_method_path__isnull=False
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
if value is True:
|
|
25
|
+
queryset = queryset.filter(has_custodian_adapter=True)
|
|
26
|
+
elif value is False:
|
|
27
|
+
queryset = queryset.filter(has_custodian_adapter=False)
|
|
28
|
+
return queryset
|
|
29
|
+
|
|
30
|
+
def filter_waiting_for_input(self, queryset, name, value):
|
|
31
|
+
input_status = [OrderProposal.Status.PENDING, OrderProposal.Status.DRAFT, OrderProposal.Status.APPROVED]
|
|
32
|
+
if value is True:
|
|
33
|
+
queryset = queryset.filter(status__in=input_status)
|
|
34
|
+
elif value is False:
|
|
35
|
+
queryset = queryset.exclude(status__in=input_status)
|
|
36
|
+
return queryset
|
|
37
|
+
|
|
38
|
+
def filter_is_automatic_rebalancing(self, queryset, name, value):
|
|
39
|
+
if value is True:
|
|
40
|
+
queryset = queryset.filter(rebalancing_model__isnull=False)
|
|
41
|
+
elif value is False:
|
|
42
|
+
queryset = queryset.filter(rebalancing_model__isnull=True)
|
|
43
|
+
return queryset
|
|
44
|
+
|
|
45
|
+
class Meta:
|
|
46
|
+
model = OrderProposal
|
|
47
|
+
fields = {
|
|
48
|
+
"trade_date": ["exact"],
|
|
49
|
+
"status": ["exact"],
|
|
50
|
+
"rebalancing_model": ["exact"],
|
|
51
|
+
"portfolio": ["exact"],
|
|
52
|
+
"creator": ["exact"],
|
|
53
|
+
"approver": ["exact"],
|
|
54
|
+
"execution_status": ["exact"],
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from wbcore import filters as wb_filters
|
|
2
|
+
|
|
3
|
+
from wbportfolio.models import Order
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OrderFilterSet(wb_filters.FilterSet):
|
|
7
|
+
has_warnings = wb_filters.BooleanFilter()
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
model = Order
|
|
11
|
+
fields = {"underlying_instrument": ["exact"], "order_type": ["exact"]}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
from django.db.models import Exists, OuterRef
|
|
1
4
|
from wbcore import filters as wb_filters
|
|
2
5
|
from wbfdm.models import Instrument
|
|
3
6
|
|
|
4
7
|
from wbportfolio.filters.assets import get_latest_asset_position
|
|
5
|
-
from wbportfolio.models import Portfolio
|
|
8
|
+
from wbportfolio.models import AssetPosition, Portfolio
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class PortfolioFilterSet(wb_filters.FilterSet):
|
|
@@ -16,12 +19,46 @@ class PortfolioFilterSet(wb_filters.FilterSet):
|
|
|
16
19
|
filter_params={"is_managed": True},
|
|
17
20
|
method="filter_instrument",
|
|
18
21
|
)
|
|
22
|
+
modeled_after = wb_filters.ModelChoiceFilter(
|
|
23
|
+
label="Modeled After",
|
|
24
|
+
queryset=Portfolio.objects.all(),
|
|
25
|
+
endpoint=Portfolio.get_representation_endpoint(),
|
|
26
|
+
value_key=Portfolio.get_representation_value_key(),
|
|
27
|
+
label_key=Portfolio.get_representation_label_key(),
|
|
28
|
+
method="filter_modeled_after",
|
|
29
|
+
)
|
|
30
|
+
invests_in = wb_filters.ModelChoiceFilter(
|
|
31
|
+
label="Invests In",
|
|
32
|
+
queryset=Instrument.objects.all(),
|
|
33
|
+
endpoint=Instrument.get_representation_endpoint(),
|
|
34
|
+
value_key=Instrument.get_representation_value_key(),
|
|
35
|
+
label_key=Instrument.get_representation_label_key(),
|
|
36
|
+
filter_params={"is_investable_universe": True},
|
|
37
|
+
method="filter_invests_in",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def filter_modeled_after(self, queryset, name, value):
|
|
41
|
+
if value:
|
|
42
|
+
modeled_after_portfolio_ids = list(
|
|
43
|
+
map(
|
|
44
|
+
lambda p: p.portfolio.id, value.get_model_portfolio_relationships(date.today() - timedelta(days=7))
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return queryset.filter(id__in=modeled_after_portfolio_ids)
|
|
48
|
+
return queryset
|
|
19
49
|
|
|
20
50
|
def filter_instrument(self, queryset, name, value):
|
|
21
51
|
if value:
|
|
22
52
|
return queryset.filter(instruments=value)
|
|
23
53
|
return queryset
|
|
24
54
|
|
|
55
|
+
def filter_invests_in(self, queryset, name, value):
|
|
56
|
+
if value:
|
|
57
|
+
return queryset.annotate(
|
|
58
|
+
invests_in=Exists(AssetPosition.objects.filter(underlying_quote=value, portfolio=OuterRef("pk")))
|
|
59
|
+
).filter(invests_in=True)
|
|
60
|
+
return queryset
|
|
61
|
+
|
|
25
62
|
class Meta:
|
|
26
63
|
model = Portfolio
|
|
27
64
|
fields = {
|
wbportfolio/filters/positions.py
CHANGED
|
@@ -76,7 +76,6 @@ class AssetPositionPandasFilter(DateFilterMixin, PandasFilterSetMixin, wb_filter
|
|
|
76
76
|
date = total_value_fx_usd = total_value_fx_usd__gte = total_value_fx_usd__lte = None
|
|
77
77
|
|
|
78
78
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
79
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
80
79
|
label="Date Range",
|
|
81
80
|
required=True,
|
|
82
81
|
clearable=False,
|
|
@@ -10,7 +10,7 @@ from .claim import (
|
|
|
10
10
|
NegativeTermimalAccountPerProductFilterSet,
|
|
11
11
|
ProfitAndLossPandasFilter,
|
|
12
12
|
)
|
|
13
|
-
from .fees import FeesAggregatedFilter, FeesFilter,
|
|
13
|
+
from .fees import FeesAggregatedFilter, FeesFilter, FeesProductFilterSet
|
|
14
14
|
from .trades import (
|
|
15
15
|
SubscriptionRedemptionFilterSet,
|
|
16
16
|
SubscriptionRedemptionPortfolioFilterSet,
|
|
@@ -18,4 +18,3 @@ from .trades import (
|
|
|
18
18
|
TradeInstrumentFilterSet,
|
|
19
19
|
TradePortfolioFilter,
|
|
20
20
|
)
|
|
21
|
-
from .transactions import TransactionFilterSet, TransactionPortfolioFilterSet
|
|
@@ -4,10 +4,10 @@ from wbcore.pandas.filterset import PandasFilterSetMixin
|
|
|
4
4
|
|
|
5
5
|
from wbportfolio.models import Fees
|
|
6
6
|
|
|
7
|
-
from .
|
|
7
|
+
from .utils import get_transaction_default_date_range
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class FeesFilter(
|
|
10
|
+
class FeesFilter(wb_filters.FilterSet):
|
|
11
11
|
"""FilterSet for Fees
|
|
12
12
|
|
|
13
13
|
Currently filters:
|
|
@@ -16,28 +16,24 @@ class FeesFilter(TransactionFilterSet):
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
fee_date = wb_filters.DateRangeFilter(
|
|
19
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
20
19
|
label="Date Range",
|
|
21
20
|
initial=get_transaction_default_date_range,
|
|
22
21
|
required=True,
|
|
23
22
|
)
|
|
24
|
-
total_value_usd__gte = total_value_usd__lte = transaction_underlying_type = transaction_date = None
|
|
25
23
|
|
|
26
24
|
class Meta:
|
|
27
25
|
model = Fees
|
|
28
26
|
fields = {
|
|
29
27
|
"transaction_subtype": ["exact"],
|
|
30
28
|
"currency_fx_rate": ["gte", "exact", "lte"],
|
|
31
|
-
"
|
|
29
|
+
"product": ["exact"],
|
|
32
30
|
"currency": ["exact"],
|
|
33
31
|
"total_value": ["gte", "exact", "lte"],
|
|
34
32
|
"total_value_fx_portfolio": ["gte", "exact", "lte"],
|
|
35
|
-
"total_value_gross": ["gte", "exact", "lte"],
|
|
36
|
-
"total_value_gross_fx_portfolio": ["gte", "exact", "lte"],
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
|
|
40
|
-
class
|
|
36
|
+
class FeesProductFilterSet(FeesFilter):
|
|
41
37
|
portfolio = None
|
|
42
38
|
|
|
43
39
|
class Meta:
|
|
@@ -49,15 +45,12 @@ class FeesPortfolioFilterSet(FeesFilter):
|
|
|
49
45
|
"currency": ["exact"],
|
|
50
46
|
"total_value": ["gte", "exact", "lte"],
|
|
51
47
|
"total_value_fx_portfolio": ["gte", "exact", "lte"],
|
|
52
|
-
"total_value_gross": ["gte", "exact", "lte"],
|
|
53
|
-
"total_value_gross_fx_portfolio": ["gte", "exact", "lte"],
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
|
|
57
51
|
class FeesAggregatedFilter(PandasFilterSetMixin, wb_filters.FilterSet):
|
|
58
|
-
|
|
52
|
+
fee_date = wb_filters.DateRangeFilter(
|
|
59
53
|
label="Date Range",
|
|
60
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
61
54
|
required=True,
|
|
62
55
|
clearable=False,
|
|
63
56
|
initial=current_quarter_date_range,
|
|
@@ -5,14 +5,27 @@ from wbcore import filters as wb_filters
|
|
|
5
5
|
from wbcrm.models.accounts import Account
|
|
6
6
|
from wbfdm.models import Classification, Instrument
|
|
7
7
|
|
|
8
|
-
from wbportfolio.models import Product, Trade
|
|
8
|
+
from wbportfolio.models import Portfolio, Product, Trade
|
|
9
9
|
from wbportfolio.models.transactions.claim import Claim
|
|
10
10
|
|
|
11
11
|
from .mixins import OppositeSharesFieldMethodMixin
|
|
12
|
-
from .
|
|
12
|
+
from .utils import get_transaction_default_date_range
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class TradeFilter(OppositeSharesFieldMethodMixin,
|
|
15
|
+
class TradeFilter(OppositeSharesFieldMethodMixin, wb_filters.FilterSet):
|
|
16
|
+
transaction_date = wb_filters.DateRangeFilter(
|
|
17
|
+
label="Date Range",
|
|
18
|
+
initial=get_transaction_default_date_range,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
portfolio = wb_filters.ModelChoiceFilter(
|
|
22
|
+
label="Portfolio",
|
|
23
|
+
queryset=Portfolio.objects.all(),
|
|
24
|
+
endpoint=Portfolio.get_representation_endpoint(),
|
|
25
|
+
value_key=Portfolio.get_representation_value_key(),
|
|
26
|
+
label_key=Portfolio.get_representation_label_key(),
|
|
27
|
+
)
|
|
28
|
+
|
|
16
29
|
# we have to redefine the mixin fields because django_filters does not allow class extension with mixin
|
|
17
30
|
opposite_shares = wb_filters.NumberFilter(
|
|
18
31
|
field_name="shares",
|
|
@@ -28,10 +41,6 @@ class TradeFilter(OppositeSharesFieldMethodMixin, TransactionFilterSet):
|
|
|
28
41
|
lookup_icon="≈±",
|
|
29
42
|
lookup_label="Opposite Approximate (+- 10%)",
|
|
30
43
|
)
|
|
31
|
-
transaction_date = wb_filters.DateRangeFilter(
|
|
32
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
33
|
-
label="Date Range",
|
|
34
|
-
)
|
|
35
44
|
underlying_instrument = wb_filters.ModelChoiceFilter(
|
|
36
45
|
label="Instrument",
|
|
37
46
|
queryset=Instrument.objects.all(),
|
|
@@ -69,7 +78,6 @@ class TradeFilter(OppositeSharesFieldMethodMixin, TransactionFilterSet):
|
|
|
69
78
|
# value_key=Register.get_representation_value_key(),
|
|
70
79
|
# label_key = Register.get_representation_label_key(),
|
|
71
80
|
# )
|
|
72
|
-
total_value_usd__gte = total_value_usd__lte = transaction_underlying_type = None
|
|
73
81
|
marked_for_deletion = wb_filters.BooleanFilter(
|
|
74
82
|
label="Marked For Deletion", initial=False, field_name="marked_for_deletion", lookup_expr="exact"
|
|
75
83
|
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
from psycopg.types.range import DateRange
|
|
4
|
+
|
|
5
|
+
from wbportfolio.models import Fees, Trade
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_transaction_gte_default(field, request, view):
|
|
9
|
+
filter_date = date.today() - timedelta(days=90)
|
|
10
|
+
qs = Trade.objects.none()
|
|
11
|
+
if "instrument_id" in view.kwargs:
|
|
12
|
+
qs = Trade.objects.filter(underlying_instrument__id=view.kwargs["instrument_id"])
|
|
13
|
+
elif "portfolio_id" in view.kwargs:
|
|
14
|
+
qs = Trade.objects.filter(portfolio__id=view.kwargs["portfolio_id"])
|
|
15
|
+
if qs.exists():
|
|
16
|
+
filter_date = qs.earliest("transaction_date").transaction_date
|
|
17
|
+
return filter_date
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_transaction_underlying_type_choices(*args):
|
|
21
|
+
models = [Fees, Trade]
|
|
22
|
+
choices = []
|
|
23
|
+
for model in models:
|
|
24
|
+
for choice in model.Type.choices:
|
|
25
|
+
choices.append(choice)
|
|
26
|
+
return choices
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_transaction_lte_default(field, request, view):
|
|
30
|
+
filter_date = date.today() + timedelta(days=7)
|
|
31
|
+
qs = Trade.objects.none()
|
|
32
|
+
if "instrument_id" in view.kwargs:
|
|
33
|
+
qs = Trade.objects.filter(underlying_instrument__id=view.kwargs["instrument_id"])
|
|
34
|
+
elif "portfolio_id" in view.kwargs:
|
|
35
|
+
qs = Trade.objects.filter(portfolio__id=view.kwargs["portfolio_id"])
|
|
36
|
+
if qs.exists():
|
|
37
|
+
filter_date = qs.latest("transaction_date").transaction_date
|
|
38
|
+
return filter_date
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_transaction_default_date_range(*args, **kwargs):
|
|
42
|
+
return DateRange(get_transaction_gte_default(*args, **kwargs), get_transaction_lte_default(*args, **kwargs))
|
|
@@ -7,7 +7,8 @@ from django.db import models
|
|
|
7
7
|
from pandas.tseries.offsets import BDay
|
|
8
8
|
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from wbportfolio.api_clients.ubs import UBSNeoAPIClient
|
|
11
|
+
|
|
11
12
|
from .mixin import DataBackendMixin
|
|
12
13
|
|
|
13
14
|
|
|
@@ -21,7 +22,8 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
21
22
|
self.ubs_bank = ubs_bank
|
|
22
23
|
if not import_credential or not import_credential.authentication_token:
|
|
23
24
|
raise ValueError("UBS backend needs a valid import credential object")
|
|
24
|
-
self.authentication_token = import_credential.authentication_token
|
|
25
|
+
self.authentication_token = import_credential.authentication_token.replace("Bearer ", "")
|
|
26
|
+
self.token_expiry_date = import_credential.validity_end
|
|
25
27
|
|
|
26
28
|
def get_files(
|
|
27
29
|
self,
|
|
@@ -31,13 +33,10 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
31
33
|
**kwargs,
|
|
32
34
|
) -> BytesIO:
|
|
33
35
|
execution_date = (execution_time - BDay(1)).date()
|
|
34
|
-
|
|
35
|
-
endpoint = "https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{0}/{1}"
|
|
36
36
|
if obj_external_ids:
|
|
37
|
+
client = UBSNeoAPIClient(self.authentication_token, self.token_expiry_date)
|
|
37
38
|
for external_id in obj_external_ids:
|
|
38
|
-
res_json =
|
|
39
|
-
self.authentication_token, endpoint.format(external_id, execution_date.strftime("%Y-%m-%d"))
|
|
40
|
-
)
|
|
39
|
+
res_json = client.validate_response(client.get_portfolio_at_date(external_id, execution_date))
|
|
41
40
|
if res_json:
|
|
42
41
|
content_file = BytesIO()
|
|
43
42
|
content_file.write(json.dumps(res_json).encode())
|
|
@@ -7,7 +7,8 @@ from django.db import models
|
|
|
7
7
|
from dynamic_preferences.registries import global_preferences_registry
|
|
8
8
|
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from wbportfolio.api_clients.ubs import UBSNeoAPIClient
|
|
11
|
+
|
|
11
12
|
from .mixin import DataBackendMixin
|
|
12
13
|
|
|
13
14
|
|
|
@@ -21,7 +22,8 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
21
22
|
self.ubs_bank = ubs_bank
|
|
22
23
|
if not import_credential or not import_credential.authentication_token:
|
|
23
24
|
raise ValueError("UBS backend needs a valid import credential object")
|
|
24
|
-
self.authentication_token = import_credential.authentication_token
|
|
25
|
+
self.authentication_token = import_credential.authentication_token.replace("Bearer ", "")
|
|
26
|
+
self.token_expiry_date = import_credential.validity_end
|
|
25
27
|
|
|
26
28
|
def get_files(
|
|
27
29
|
self,
|
|
@@ -30,32 +32,20 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
30
32
|
**kwargs,
|
|
31
33
|
) -> BytesIO:
|
|
32
34
|
execution_date = execution_time.date()
|
|
33
|
-
|
|
34
|
-
mngt_fees_endpoint = "https://neo.ubs.com/api/ged-amc/external/fee/v1/management/{0}"
|
|
35
|
-
perf_fees_endpoint = "https://neo.ubs.com/api/ged-amc/external/fee/v1/performance/{0}"
|
|
36
35
|
if obj_external_ids:
|
|
36
|
+
client = UBSNeoAPIClient(self.authentication_token, self.token_expiry_date)
|
|
37
|
+
start = kwargs.get("start", None)
|
|
38
|
+
if not start:
|
|
39
|
+
start = global_preferences_registry.manager()["wbfdm__default_start_date_historical_import"]
|
|
37
40
|
for external_id in obj_external_ids:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
start = global_preferences_registry.manager()["wbfdm__default_start_date_historical_import"]
|
|
41
|
-
mngt_res = process_request(
|
|
42
|
-
self.authentication_token,
|
|
43
|
-
mngt_fees_endpoint.format(external_id),
|
|
44
|
-
{"fromDate": start.strftime("%Y-%m-%d"), "toDate": execution_date.strftime("%Y-%m-%d")},
|
|
45
|
-
)
|
|
46
|
-
perf_res = process_request(
|
|
47
|
-
self.authentication_token,
|
|
48
|
-
perf_fees_endpoint.format(external_id),
|
|
49
|
-
{"fromDate": start.strftime("%Y-%m-%d"), "toDate": execution_date.strftime("%Y-%m-%d")},
|
|
50
|
-
)
|
|
51
|
-
|
|
41
|
+
mngt_res = client.validate_response(client.get_management_fees(external_id, start, execution_date))
|
|
42
|
+
perf_res = client.validate_response(client.get_performance_fees(external_id, start, execution_date))
|
|
52
43
|
if mngt_res or perf_res:
|
|
53
44
|
res_json = {
|
|
54
45
|
"performance_fees": perf_res.get("fees", []),
|
|
55
46
|
"management_fees": mngt_res.get("fees", []),
|
|
56
47
|
"isin": external_id,
|
|
57
48
|
}
|
|
58
|
-
|
|
59
49
|
if res_json:
|
|
60
50
|
content_file = BytesIO()
|
|
61
51
|
content_file.write(json.dumps(res_json).encode())
|
|
@@ -7,7 +7,8 @@ from django.db import models
|
|
|
7
7
|
from pandas.tseries.offsets import BDay
|
|
8
8
|
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from wbportfolio.api_clients.ubs import UBSNeoAPIClient
|
|
11
|
+
|
|
11
12
|
from .mixin import DataBackendMixin
|
|
12
13
|
|
|
13
14
|
|
|
@@ -21,7 +22,8 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
21
22
|
self.ubs_bank = ubs_bank
|
|
22
23
|
if not import_credential or not import_credential.authentication_token:
|
|
23
24
|
raise ValueError("UBS backend needs a valid import credential object")
|
|
24
|
-
self.authentication_token = import_credential.authentication_token
|
|
25
|
+
self.authentication_token = import_credential.authentication_token.replace("Bearer ", "")
|
|
26
|
+
self.token_expiry_date = import_credential.validity_end
|
|
25
27
|
|
|
26
28
|
def get_files(
|
|
27
29
|
self,
|
|
@@ -30,12 +32,10 @@ class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
|
30
32
|
**kwargs,
|
|
31
33
|
) -> BytesIO:
|
|
32
34
|
execution_date = (execution_time - BDay(1)).date()
|
|
33
|
-
endpoint = "https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{0}/{1}"
|
|
34
35
|
if obj_external_ids:
|
|
36
|
+
client = UBSNeoAPIClient(self.authentication_token, self.token_expiry_date)
|
|
35
37
|
for external_id in obj_external_ids:
|
|
36
|
-
res_json =
|
|
37
|
-
self.authentication_token, endpoint.format(external_id, execution_date.strftime("%Y-%m-%d"))
|
|
38
|
-
)
|
|
38
|
+
res_json = client.validate_response(client.get_portfolio_at_date(external_id, execution_date))
|
|
39
39
|
res_json.pop("constituents", None)
|
|
40
40
|
if res_json:
|
|
41
41
|
content_file = BytesIO()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from django.db import models
|
|
7
|
+
from pandas.tseries.offsets import BDay
|
|
8
|
+
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
|
+
|
|
10
|
+
from wbportfolio.api_clients.ubs import UBSNeoAPIClient
|
|
11
|
+
|
|
12
|
+
from .mixin import DataBackendMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("Trade", provider_key="ubs", save_data_in_import_source=True, passive_only=True)
|
|
16
|
+
class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
17
|
+
def __init__(
|
|
18
|
+
self, import_credential: Optional[models.Model] = None, ubs_bank: Optional[models.Model] = None, **kwargs
|
|
19
|
+
):
|
|
20
|
+
if not ubs_bank:
|
|
21
|
+
raise ValueError("The ubs company objects needs to be passed to this backend")
|
|
22
|
+
self.ubs_bank = ubs_bank
|
|
23
|
+
if not import_credential or not import_credential.authentication_token:
|
|
24
|
+
raise ValueError("UBS backend needs a valid import credential object")
|
|
25
|
+
self.authentication_token = import_credential.authentication_token.replace("Bearer ", "")
|
|
26
|
+
self.token_expiry_date = import_credential.validity_end
|
|
27
|
+
|
|
28
|
+
def get_files(
|
|
29
|
+
self,
|
|
30
|
+
execution_time: datetime,
|
|
31
|
+
start: date = None,
|
|
32
|
+
obj_external_ids: list[str] = None,
|
|
33
|
+
**kwargs,
|
|
34
|
+
) -> BytesIO:
|
|
35
|
+
if not start:
|
|
36
|
+
start = (execution_time - BDay(2)).date()
|
|
37
|
+
end = execution_time.date()
|
|
38
|
+
if obj_external_ids:
|
|
39
|
+
client = UBSNeoAPIClient(self.authentication_token, self.token_expiry_date)
|
|
40
|
+
for external_id in obj_external_ids:
|
|
41
|
+
res_json = client.validate_response(
|
|
42
|
+
client.get_rebalance_reports(external_id, from_date=start, to_date=end)
|
|
43
|
+
)
|
|
44
|
+
if res_json:
|
|
45
|
+
content_file = BytesIO()
|
|
46
|
+
content_file.write(json.dumps(res_json).encode())
|
|
47
|
+
file_name = f"ubs_trades_{external_id}_{start:%Y-%m-%d}-{end:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
|
|
48
|
+
yield file_name, content_file
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
from contextlib import suppress
|
|
2
|
-
|
|
3
|
-
import pandas as pd
|
|
4
|
-
import requests
|
|
5
1
|
from django.db.models import Q
|
|
6
2
|
from dynamic_preferences.registries import global_preferences_registry
|
|
7
3
|
from wbfdm.models import Instrument
|
|
@@ -11,19 +7,6 @@ def get_timedelta_import_instrument_price():
|
|
|
11
7
|
return global_preferences_registry.manager()["wbportfolio__timedelta_import_instrument_price"]
|
|
12
8
|
|
|
13
9
|
|
|
14
|
-
def process_request(authentication_token: str, endpoint: str | None = None, kwargs={}) -> pd.DataFrame:
|
|
15
|
-
headers = {"Authorization": authentication_token}
|
|
16
|
-
r = requests.get(endpoint, params=kwargs, headers=headers)
|
|
17
|
-
if r.status_code == requests.codes.ok:
|
|
18
|
-
with suppress(
|
|
19
|
-
requests.exceptions.JSONDecodeError
|
|
20
|
-
): # we catch any json decode error because the UBS api doesn't seem to respect HTTP status code rule (i.e. returns 200 even though the http content is malformed)
|
|
21
|
-
r_json = r.json()
|
|
22
|
-
if r_json.get("status", "") == "SUCCESS":
|
|
23
|
-
return r_json
|
|
24
|
-
raise ValueError(f"Issue while processing request: {r.content}")
|
|
25
|
-
|
|
26
|
-
|
|
27
10
|
def filter_active_instruments(_date, queryset=None):
|
|
28
11
|
if not queryset:
|
|
29
12
|
queryset = Instrument.objects
|