wbportfolio 1.55.8__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/orders/order_proposals.py +2 -0
- wbportfolio/admin/orders/orders.py +2 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/api_clients/ubs.py +23 -11
- 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/factories/assets.py +1 -1
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +8 -3
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/filters/assets.py +0 -1
- wbportfolio/filters/orders/order_proposals.py +3 -6
- wbportfolio/filters/portfolios.py +18 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/fees.py +0 -2
- wbportfolio/filters/transactions/trades.py +0 -1
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/handlers/asset_position.py +9 -5
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +4 -4
- 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 +2 -2
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
- 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/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/resources/trades.py +1 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- 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/adjustments.py +1 -1
- wbportfolio/models/asset.py +7 -3
- wbportfolio/models/builder.py +25 -5
- 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/liquidity_stress_test.py +1 -1
- wbportfolio/models/orders/order_proposals.py +620 -490
- wbportfolio/models/orders/orders.py +237 -75
- wbportfolio/models/portfolio.py +79 -18
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +4 -1
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/trades.py +4 -0
- wbportfolio/models/transactions/transactions.py +16 -4
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +16 -0
- wbportfolio/order_routing/adapters/__init__.py +14 -6
- wbportfolio/order_routing/adapters/ubs.py +104 -70
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +115 -103
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- 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/orders/order_proposals.py +6 -2
- wbportfolio/serializers/orders/orders.py +119 -26
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/tasks.py +42 -4
- wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
- wbportfolio/tests/models/test_portfolios.py +9 -9
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
- wbportfolio/tests/rebalancing/test_models.py +2 -2
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +1 -1
- wbportfolio/viewsets/charts/assets.py +8 -4
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
- wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
- wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
- wbportfolio/viewsets/orders/order_proposals.py +92 -21
- wbportfolio/viewsets/orders/orders.py +79 -26
- wbportfolio/viewsets/portfolios.py +24 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/fdm/tasks.py +0 -42
- wbportfolio/models/orders/routing.py +0 -54
- wbportfolio/pms/trading/handler.py +0 -211
- /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from pandas._libs.tslibs.offsets import BDay
|
|
6
|
+
|
|
7
|
+
from wbportfolio.models import AssetPosition, Order, OrderProposal, Portfolio
|
|
8
|
+
from wbportfolio.models.utils import adjust_assets, adjust_orders, adjust_quote, get_adjusted_shares
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_get_adjusted_shares():
|
|
12
|
+
assert get_adjusted_shares(Decimal("150"), Decimal("100"), Decimal("200")) == Decimal("75")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.django_db
|
|
16
|
+
@patch.object(Order, "_get_price")
|
|
17
|
+
def test_adjust_orders(mock_get_price, weekday, order_factory, instrument_factory):
|
|
18
|
+
mock_get_price.return_value = (Decimal("100"), Decimal("0"))
|
|
19
|
+
old_quote = instrument_factory.create()
|
|
20
|
+
new_quote = instrument_factory.create()
|
|
21
|
+
o1 = order_factory.create(
|
|
22
|
+
underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
|
|
23
|
+
)
|
|
24
|
+
o2 = order_factory.create(
|
|
25
|
+
underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("20")
|
|
26
|
+
)
|
|
27
|
+
adjust_orders(Order.objects.filter(underlying_instrument=old_quote), new_quote)
|
|
28
|
+
|
|
29
|
+
o1.refresh_from_db()
|
|
30
|
+
o2.refresh_from_db()
|
|
31
|
+
|
|
32
|
+
assert o1.underlying_instrument == new_quote
|
|
33
|
+
assert o2.underlying_instrument == new_quote
|
|
34
|
+
assert o1.price == Decimal("100")
|
|
35
|
+
assert o2.price == Decimal("100")
|
|
36
|
+
assert o1.shares == Decimal("10")
|
|
37
|
+
assert o2.shares == Decimal("20")
|
|
38
|
+
|
|
39
|
+
mock_get_price.return_value = (Decimal("200"), Decimal("0"))
|
|
40
|
+
adjust_orders(Order.objects.filter(underlying_instrument=new_quote), old_quote)
|
|
41
|
+
|
|
42
|
+
o1.refresh_from_db()
|
|
43
|
+
o2.refresh_from_db()
|
|
44
|
+
|
|
45
|
+
assert o1.underlying_instrument == old_quote
|
|
46
|
+
assert o2.underlying_instrument == old_quote
|
|
47
|
+
assert o1.price == Decimal("200")
|
|
48
|
+
assert o2.price == Decimal("200")
|
|
49
|
+
assert o1.shares == Decimal("5")
|
|
50
|
+
assert o2.shares == Decimal("10")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.django_db
|
|
54
|
+
def test_adjust_assets(weekday, asset_position_factory, instrument_factory, instrument_price_factory):
|
|
55
|
+
old_quote = instrument_factory.create()
|
|
56
|
+
new_quote = instrument_factory.create()
|
|
57
|
+
old_quote_price = instrument_price_factory.create(
|
|
58
|
+
instrument=old_quote, net_value=Decimal("100"), date=weekday, calculated=False
|
|
59
|
+
)
|
|
60
|
+
new_quote_price = instrument_price_factory.create(
|
|
61
|
+
instrument=new_quote, net_value=Decimal("100"), date=weekday, calculated=False
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
a1 = asset_position_factory.create(
|
|
65
|
+
underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("10")
|
|
66
|
+
)
|
|
67
|
+
a2 = asset_position_factory.create(
|
|
68
|
+
underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("20")
|
|
69
|
+
)
|
|
70
|
+
adjust_assets(AssetPosition.objects.filter(underlying_quote=old_quote), new_quote)
|
|
71
|
+
|
|
72
|
+
a1.refresh_from_db()
|
|
73
|
+
a2.refresh_from_db()
|
|
74
|
+
|
|
75
|
+
assert a1.underlying_quote == new_quote
|
|
76
|
+
assert a2.underlying_quote == new_quote
|
|
77
|
+
assert a1.underlying_quote_price == new_quote_price
|
|
78
|
+
assert a2.underlying_quote_price == new_quote_price
|
|
79
|
+
assert a1.initial_price == Decimal("100")
|
|
80
|
+
assert a2.initial_price == Decimal("100")
|
|
81
|
+
assert a1.initial_shares == Decimal("10")
|
|
82
|
+
assert a2.initial_shares == Decimal("20")
|
|
83
|
+
|
|
84
|
+
old_quote_price.net_value = new_quote_price.net_value = Decimal("200")
|
|
85
|
+
old_quote_price.save()
|
|
86
|
+
new_quote_price.save()
|
|
87
|
+
|
|
88
|
+
adjust_assets(AssetPosition.objects.filter(underlying_quote=new_quote), old_quote)
|
|
89
|
+
|
|
90
|
+
a1.refresh_from_db()
|
|
91
|
+
a2.refresh_from_db()
|
|
92
|
+
|
|
93
|
+
assert a1.underlying_quote == old_quote
|
|
94
|
+
assert a2.underlying_quote == old_quote
|
|
95
|
+
assert a1.underlying_quote_price == old_quote_price
|
|
96
|
+
assert a2.underlying_quote_price == old_quote_price
|
|
97
|
+
assert a1.initial_price == Decimal("200")
|
|
98
|
+
assert a2.initial_price == Decimal("200")
|
|
99
|
+
assert a1.initial_shares == Decimal("5")
|
|
100
|
+
assert a2.initial_shares == Decimal("10")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.django_db
|
|
104
|
+
@patch("wbportfolio.models.utils.adjust_assets")
|
|
105
|
+
@patch("wbportfolio.models.utils.adjust_orders")
|
|
106
|
+
@patch.object(OrderProposal, "replay")
|
|
107
|
+
def test_adjust_quote(mock_replay, mock_adjust_orders, mock_adjust_assets, weekday, order_factory, instrument_factory):
|
|
108
|
+
old_quote = instrument_factory.create()
|
|
109
|
+
new_quote = instrument_factory.create()
|
|
110
|
+
o1 = order_factory.create( # noqa: F841
|
|
111
|
+
underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
|
|
112
|
+
)
|
|
113
|
+
o2 = order_factory.create( # noqa: F841
|
|
114
|
+
underlying_instrument=old_quote,
|
|
115
|
+
order_proposal__trade_date=(weekday + BDay(1)),
|
|
116
|
+
price=Decimal("100"),
|
|
117
|
+
shares=Decimal("10"),
|
|
118
|
+
)
|
|
119
|
+
o3 = order_factory.create(
|
|
120
|
+
underlying_instrument=old_quote,
|
|
121
|
+
order_proposal__trade_date=(weekday + BDay(1)),
|
|
122
|
+
price=Decimal("100"),
|
|
123
|
+
shares=Decimal("10"),
|
|
124
|
+
)
|
|
125
|
+
adjust_quote(
|
|
126
|
+
old_quote,
|
|
127
|
+
new_quote,
|
|
128
|
+
adjust_after=weekday,
|
|
129
|
+
only_portfolios=Portfolio.objects.filter(id=o3.order_proposal.portfolio.id),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
mock_adjust_assets.asser_called_once()
|
|
133
|
+
assert set(mock_adjust_assets.call_args[0][0]) == set(AssetPosition.objects.none())
|
|
134
|
+
assert mock_adjust_assets.call_args[0][1] == new_quote
|
|
135
|
+
|
|
136
|
+
mock_adjust_orders.assert_called_once()
|
|
137
|
+
assert set(mock_adjust_orders.call_args[0][0]) == set(Order.objects.filter(id=o3.id))
|
|
138
|
+
assert mock_adjust_orders.call_args[0][1] == new_quote
|
|
139
|
+
|
|
140
|
+
mock_replay.assert_called_once_with(reapply_order_proposal=True)
|
|
@@ -65,7 +65,7 @@ class TestRebalancer:
|
|
|
65
65
|
)
|
|
66
66
|
order_proposal = rebalancer.evaluate_rebalancing(trade_date)
|
|
67
67
|
assert order_proposal.orders.count() == 2
|
|
68
|
-
assert order_proposal.status == OrderProposal.Status.
|
|
68
|
+
assert order_proposal.status == OrderProposal.Status.CONFIRMED
|
|
69
69
|
assert AssetPosition.objects.get(
|
|
70
70
|
portfolio=rebalancer.portfolio, date=trade_date, underlying_quote=a1.underlying_instrument
|
|
71
71
|
).weighting == Decimal(0.5)
|
|
@@ -82,7 +82,7 @@ class TestCompositeRebalancing:
|
|
|
82
82
|
assert not model.is_valid()
|
|
83
83
|
|
|
84
84
|
order_proposal = OrderProposalFactory.create(
|
|
85
|
-
portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.
|
|
85
|
+
portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.CONFIRMED
|
|
86
86
|
)
|
|
87
87
|
t1 = OrderFactory.create(
|
|
88
88
|
portfolio=model.portfolio,
|
|
@@ -104,7 +104,7 @@ class TestCompositeRebalancing:
|
|
|
104
104
|
|
|
105
105
|
def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
|
|
106
106
|
order_proposal = OrderProposalFactory.create(
|
|
107
|
-
portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.
|
|
107
|
+
portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.CONFIRMED
|
|
108
108
|
)
|
|
109
109
|
asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
|
|
110
110
|
asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
|
|
@@ -23,6 +23,7 @@ class TestProductModelViewSet:
|
|
|
23
23
|
portfolio_factory.create_batch(4, invested_timespan=DateRange(date.min, date.max))
|
|
24
24
|
+ portfolio_factory.create_batch(2, invested_timespan=DateRange(date.min, date.max)),
|
|
25
25
|
product_factory.create_batch(6),
|
|
26
|
+
strict=False,
|
|
26
27
|
):
|
|
27
28
|
InstrumentPortfolioThroughModel.objects.update_or_create(
|
|
28
29
|
instrument=product, defaults={"portfolio": portfolio}
|
wbportfolio/urls.py
CHANGED
|
@@ -139,7 +139,7 @@ product_router.register(r"fees", viewsets.FeesProductModelViewSet, basename="pro
|
|
|
139
139
|
|
|
140
140
|
# Subrouter for Order Proposal
|
|
141
141
|
order_proposal_router = WBCoreRouter()
|
|
142
|
-
order_proposal_router.register(r"
|
|
142
|
+
order_proposal_router.register(r"order", viewsets.OrderOrderProposalModelViewSet, basename="orderproposal-order")
|
|
143
143
|
|
|
144
144
|
trade_router = WBCoreRouter()
|
|
145
145
|
trade_router.register(r"claim", viewsets.ClaimTradeModelViewSet, basename="trade-claim")
|
|
@@ -101,7 +101,9 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
101
101
|
columns = {}
|
|
102
102
|
level_representations = self.classification_group.get_levels_representation()
|
|
103
103
|
for key, label in zip(
|
|
104
|
-
reversed(self.classification_group.get_fields_names(sep="_")),
|
|
104
|
+
reversed(self.classification_group.get_fields_names(sep="_")),
|
|
105
|
+
reversed(level_representations[1:]),
|
|
106
|
+
strict=False,
|
|
105
107
|
):
|
|
106
108
|
columns[key] = label
|
|
107
109
|
columns["label"] = "Classification"
|
|
@@ -158,6 +160,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
158
160
|
return df.reset_index(names="id")
|
|
159
161
|
|
|
160
162
|
def manipulate_dataframe(self, df):
|
|
163
|
+
df["id"] = df["id"].fillna(-1)
|
|
161
164
|
if self.group_by == AssetPositionGroupBy.INDUSTRY:
|
|
162
165
|
if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
|
|
163
166
|
df["equity"] = ""
|
|
@@ -169,8 +172,8 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
169
172
|
dict(Classification.objects.filter(id__in=df["classification"].dropna()).values_list("id", "name"))
|
|
170
173
|
)
|
|
171
174
|
elif self.group_by == AssetPositionGroupBy.CASH:
|
|
172
|
-
df.loc[df["id"]
|
|
173
|
-
df.loc[df["id"]
|
|
175
|
+
df.loc[df["id"], "label"] = "Cash"
|
|
176
|
+
df.loc[~df["id"], "label"] = "Non-Cash"
|
|
174
177
|
elif self.group_by == AssetPositionGroupBy.COUNTRY:
|
|
175
178
|
df["label"] = df["id"].map(dict(Geography.objects.filter(id__in=df["id"]).values_list("id", "name")))
|
|
176
179
|
elif self.group_by == AssetPositionGroupBy.CURRENCY:
|
|
@@ -180,6 +183,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
180
183
|
df["label"] = df["id"].map(
|
|
181
184
|
dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
|
|
182
185
|
)
|
|
186
|
+
df.loc[df["id"] == -1, "label"] = "N/A"
|
|
183
187
|
df.sort_values(by="weighting", ascending=False, inplace=True)
|
|
184
188
|
return df
|
|
185
189
|
|
|
@@ -307,7 +311,7 @@ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewset
|
|
|
307
311
|
|
|
308
312
|
text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
|
|
309
313
|
text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
|
|
310
|
-
|
|
314
|
+
self.nb_rows = df.shape[0]
|
|
311
315
|
fig.add_trace(
|
|
312
316
|
go.Bar(
|
|
313
317
|
y=df.instrument_id,
|
|
@@ -96,7 +96,7 @@ class AssetPositionPortfolioButtonConfig(AssetPositionButtonConfig):
|
|
|
96
96
|
request=self.request,
|
|
97
97
|
)
|
|
98
98
|
),
|
|
99
|
-
label=f"{PortfolioPortfolioThroughModel.Type[rel.type].label}
|
|
99
|
+
label=f"Dependency Portfolio ({PortfolioPortfolioThroughModel.Type[rel.type].label})",
|
|
100
100
|
)
|
|
101
101
|
)
|
|
102
102
|
return set(btns)
|
|
@@ -7,7 +7,7 @@ from wbfdm.models.instruments import Instrument
|
|
|
7
7
|
|
|
8
8
|
class InstrumentButtonMixin:
|
|
9
9
|
@classmethod
|
|
10
|
-
def add_instrument_request_button(
|
|
10
|
+
def add_instrument_request_button(cls, request=None, view=None, pk=None, **kwargs):
|
|
11
11
|
buttons = [
|
|
12
12
|
bt.WidgetButton(key="assets", label="Implemented Portfolios (Assets)"),
|
|
13
13
|
# bt.WidgetButton(
|
|
@@ -44,7 +44,7 @@ class InstrumentButtonMixin:
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
|
-
def add_transactions_request_button(
|
|
47
|
+
def add_transactions_request_button(cls, request=None, view=None, pk=None, **kwargs):
|
|
48
48
|
return bt.DropDownButton(
|
|
49
49
|
label="Transactions",
|
|
50
50
|
icon=WBIcon.UNFOLD.icon,
|
|
@@ -2,6 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date
|
|
3
3
|
|
|
4
4
|
from pandas._libs.tslibs.offsets import BDay
|
|
5
|
+
from rest_framework.reverse import reverse
|
|
5
6
|
from wbcore import serializers as wb_serializers
|
|
6
7
|
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
7
8
|
from wbcore.contrib.icons import WBIcon
|
|
@@ -11,9 +12,11 @@ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
|
11
12
|
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
12
13
|
create_simple_display,
|
|
13
14
|
)
|
|
15
|
+
from wbfdm.models import Instrument
|
|
16
|
+
from wbfdm.serializers import InvestableUniverseRepresentationSerializer
|
|
14
17
|
|
|
15
18
|
from wbportfolio.models import AssetPosition, Portfolio
|
|
16
|
-
from wbportfolio.serializers import RebalancerModelSerializer
|
|
19
|
+
from wbportfolio.serializers import PortfolioRepresentationSerializer, RebalancerModelSerializer
|
|
17
20
|
from wbportfolio.viewsets.configs.display.rebalancing import RebalancerDisplayConfig
|
|
18
21
|
|
|
19
22
|
|
|
@@ -32,6 +35,28 @@ class CreateModelPortfolioSerializer(wb_serializers.ModelSerializer):
|
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
|
|
38
|
+
class AdjustQuoteSerializer(wb_serializers.Serializer):
|
|
39
|
+
old_quote = wb_serializers.PrimaryKeyRelatedField(queryset=Instrument.objects.all())
|
|
40
|
+
_old_quote = InvestableUniverseRepresentationSerializer(source="old_quote")
|
|
41
|
+
|
|
42
|
+
new_quote = wb_serializers.PrimaryKeyRelatedField(queryset=Instrument.objects.all())
|
|
43
|
+
_new_quote = InvestableUniverseRepresentationSerializer(
|
|
44
|
+
source="new_quote",
|
|
45
|
+
optional_get_parameters={"old_quote": "sibling_of"},
|
|
46
|
+
depends_on=[{"field": "old_quote", "options": {}}],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
only_portfolios = wb_serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all(), required=False)
|
|
50
|
+
_only_portfolios = PortfolioRepresentationSerializer(
|
|
51
|
+
source="only_portfolios",
|
|
52
|
+
optional_get_parameters={"old_quote": "invests_in"},
|
|
53
|
+
depends_on=[{"field": "new_quote", "options": {}}],
|
|
54
|
+
many=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
adjust_after = wb_serializers.DateField(required=False)
|
|
58
|
+
|
|
59
|
+
|
|
35
60
|
def _get_portfolio_start_end_serializer_class(portfolio):
|
|
36
61
|
today = date.today()
|
|
37
62
|
|
|
@@ -60,6 +85,25 @@ def _get_rebalance_serializer_class(portfolio):
|
|
|
60
85
|
|
|
61
86
|
|
|
62
87
|
class PortfolioButtonConfig(ButtonViewConfig):
|
|
88
|
+
def get_custom_buttons(self) -> set:
|
|
89
|
+
return {
|
|
90
|
+
bt.ActionButton(
|
|
91
|
+
method=RequestType.POST,
|
|
92
|
+
identifiers=("wbportfolio:portfolio",),
|
|
93
|
+
endpoint=reverse("wbportfolio:portfolio-adjustquote", args=[], request=self.request),
|
|
94
|
+
label="Adjust quote",
|
|
95
|
+
serializer=AdjustQuoteSerializer,
|
|
96
|
+
action_label="Action triggered",
|
|
97
|
+
title="Adjust Quote",
|
|
98
|
+
instance_display=create_simple_display(
|
|
99
|
+
[
|
|
100
|
+
["old_quote", "new_quote", "adjust_after"],
|
|
101
|
+
["only_portfolios", "only_portfolios", "only_portfolios"],
|
|
102
|
+
]
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
def get_custom_instance_buttons(self):
|
|
64
108
|
admin_buttons = [
|
|
65
109
|
bt.ActionButton(
|
|
@@ -49,14 +49,14 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
|
|
|
49
49
|
]
|
|
50
50
|
editable = [dp.FormattingRule(style={"fontWeight": "bold"})]
|
|
51
51
|
equal = [dp.FormattingRule(condition=("==", False, "is_equal"), style={"backgroundColor": "orange"})]
|
|
52
|
-
|
|
52
|
+
border_left = [
|
|
53
53
|
dp.FormattingRule(
|
|
54
54
|
style={
|
|
55
55
|
"borderLeft": "1px solid #bdc3c7",
|
|
56
56
|
}
|
|
57
57
|
)
|
|
58
58
|
]
|
|
59
|
-
|
|
59
|
+
border_right = [
|
|
60
60
|
dp.FormattingRule(
|
|
61
61
|
style={
|
|
62
62
|
"borderRight": "1px solid #bdc3c7",
|
|
@@ -111,7 +111,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
|
|
|
111
111
|
key="shares_external",
|
|
112
112
|
label="Shares",
|
|
113
113
|
width=90,
|
|
114
|
-
formatting_rules=[*equal, *editable, *
|
|
114
|
+
formatting_rules=[*equal, *editable, *border_left],
|
|
115
115
|
),
|
|
116
116
|
dp.Field(
|
|
117
117
|
key="nominal_value_external",
|
|
@@ -128,7 +128,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
|
|
|
128
128
|
key="assets_under_management_external",
|
|
129
129
|
label="AuM",
|
|
130
130
|
width=120,
|
|
131
|
-
formatting_rules=[*equal, *
|
|
131
|
+
formatting_rules=[*equal, *border_right],
|
|
132
132
|
),
|
|
133
133
|
],
|
|
134
134
|
),
|
wbportfolio/viewsets/esg.py
CHANGED
|
@@ -58,12 +58,10 @@ class ESGMetricAggregationPortfolioPandasViewSet(UserPortfolioRequestPermissionM
|
|
|
58
58
|
df = (
|
|
59
59
|
pd.DataFrame(
|
|
60
60
|
AssetPosition.objects.filter(portfolio=self.portfolio, date=self.val_date)
|
|
61
|
-
.exclude(
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
.values("underlying_instrument", "weighting", "total_value_fx_usd")
|
|
61
|
+
.exclude(Q(underlying_quote__is_cash=True) | Q(underlying_quote__is_cash_equivalent=True))
|
|
62
|
+
.values("underlying_quote", "weighting", "total_value_fx_usd")
|
|
65
63
|
)
|
|
66
|
-
.groupby("
|
|
64
|
+
.groupby("underlying_quote")
|
|
67
65
|
.sum()
|
|
68
66
|
.astype(float)
|
|
69
67
|
)
|
|
@@ -9,6 +9,7 @@ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
|
9
9
|
from wbcore.metadata.configs.display import create_simple_display
|
|
10
10
|
|
|
11
11
|
from wbportfolio.models import OrderProposal, Portfolio
|
|
12
|
+
from wbportfolio.order_routing import ExecutionStatus
|
|
12
13
|
from wbportfolio.serializers import PortfolioRepresentationSerializer
|
|
13
14
|
|
|
14
15
|
|
|
@@ -24,12 +25,61 @@ class ResetSerializer(wb_serializers.Serializer):
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
class ExecuteSerializer(wb_serializers.Serializer):
|
|
29
|
+
prioritize_target_weight = wb_serializers.BooleanField(
|
|
30
|
+
default=False,
|
|
31
|
+
label="Prioritize Target Weight",
|
|
32
|
+
help_text="If True, we will communicate to the custodian to prioritize target weight in case both shares and target weight are communicated.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
27
36
|
class OrderProposalButtonConfig(ButtonViewConfig):
|
|
28
37
|
def get_custom_list_instance_buttons(self):
|
|
29
38
|
buttons = []
|
|
30
39
|
with suppress(AttributeError, AssertionError):
|
|
31
40
|
order_proposal = self.view.get_object()
|
|
32
|
-
if order_proposal.
|
|
41
|
+
if order_proposal.can_execute(self.request.user):
|
|
42
|
+
buttons.append(
|
|
43
|
+
bt.ActionButton(
|
|
44
|
+
method=RequestType.PATCH,
|
|
45
|
+
identifiers=("wbportfolio:order",),
|
|
46
|
+
icon=WBIcon.DEAL_MONEY.icon,
|
|
47
|
+
endpoint=reverse("wbportfolio:orderproposal-execute", args=[order_proposal.id]),
|
|
48
|
+
label="Execute",
|
|
49
|
+
action_label="Execute",
|
|
50
|
+
description_fields="<p>Execute the orders through the setup custodian.</p>",
|
|
51
|
+
serializer=ExecuteSerializer,
|
|
52
|
+
instance_display=create_simple_display([["prioritize_target_weight"]]),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
elif order_proposal.status == OrderProposal.Status.EXECUTION and order_proposal.execution_status in [
|
|
57
|
+
ExecutionStatus.PENDING,
|
|
58
|
+
ExecutionStatus.IN_DRAFT,
|
|
59
|
+
]:
|
|
60
|
+
buttons.append(
|
|
61
|
+
bt.ActionButton(
|
|
62
|
+
method=RequestType.PATCH,
|
|
63
|
+
identifiers=("wbportfolio:order",),
|
|
64
|
+
icon=WBIcon.PREVIOUS.icon,
|
|
65
|
+
endpoint=reverse("wbportfolio:orderproposal-cancelexecution", args=[order_proposal.id]),
|
|
66
|
+
label="Cancel Execution",
|
|
67
|
+
action_label="Cancel Execution",
|
|
68
|
+
description_fields="<p>Cancel the current requested execution. Time sensitive operation.</p>",
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
buttons.append(
|
|
72
|
+
bt.ActionButton(
|
|
73
|
+
method=RequestType.PATCH,
|
|
74
|
+
identifiers=("wbportfolio:order",),
|
|
75
|
+
icon=WBIcon.REFRESH.icon,
|
|
76
|
+
endpoint=reverse("wbportfolio:orderproposal-updateexecutionstatus", args=[order_proposal.id]),
|
|
77
|
+
label="Update Execution Status",
|
|
78
|
+
action_label="Update Execution Status",
|
|
79
|
+
description_fields="<p>Update Execution Status.<p>",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
if order_proposal.can_be_confirmed and order_proposal.portfolio.is_model:
|
|
33
83
|
|
|
34
84
|
class PushModelChangeSerializer(wb_serializers.Serializer):
|
|
35
85
|
only_for_portfolio_ids = wb_serializers.PrimaryKeyRelatedField(
|
|
@@ -48,7 +98,7 @@ class OrderProposalButtonConfig(ButtonViewConfig):
|
|
|
48
98
|
buttons.append(
|
|
49
99
|
bt.ActionButton(
|
|
50
100
|
method=RequestType.PATCH,
|
|
51
|
-
identifiers=("wbportfolio:
|
|
101
|
+
identifiers=("wbportfolio:order",),
|
|
52
102
|
endpoint=reverse("wbportfolio:orderproposal-pushmodelchange", args=[order_proposal.id]),
|
|
53
103
|
icon=WBIcon.BROADCAST.icon,
|
|
54
104
|
label="Push Model Changes",
|
|
@@ -63,13 +113,25 @@ class OrderProposalButtonConfig(ButtonViewConfig):
|
|
|
63
113
|
),
|
|
64
114
|
)
|
|
65
115
|
)
|
|
116
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
117
|
+
buttons.append(
|
|
118
|
+
bt.ActionButton(
|
|
119
|
+
method=RequestType.PATCH,
|
|
120
|
+
identifiers=("wbportfolio:order",),
|
|
121
|
+
endpoint=reverse("wbportfolio:orderproposal-refreshpretradechecks", args=[order_proposal.id]),
|
|
122
|
+
icon=WBIcon.RUNNING.icon,
|
|
123
|
+
label="Evaluate Pre-Trade Checks",
|
|
124
|
+
action_label="Checks are running.",
|
|
125
|
+
title="Evaluate Pre-Trade Checks",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
66
128
|
buttons.append(
|
|
67
129
|
bt.DropDownButton(
|
|
68
130
|
label="Tools",
|
|
69
131
|
buttons=(
|
|
70
132
|
bt.ActionButton(
|
|
71
133
|
method=RequestType.PATCH,
|
|
72
|
-
identifiers=("wbportfolio:
|
|
134
|
+
identifiers=("wbportfolio:order",),
|
|
73
135
|
key="replay",
|
|
74
136
|
icon=WBIcon.SYNCHRONIZE.icon,
|
|
75
137
|
label="Replay Orders",
|
|
@@ -81,7 +143,7 @@ class OrderProposalButtonConfig(ButtonViewConfig):
|
|
|
81
143
|
),
|
|
82
144
|
bt.ActionButton(
|
|
83
145
|
method=RequestType.PATCH,
|
|
84
|
-
identifiers=("wbportfolio:
|
|
146
|
+
identifiers=("wbportfolio:order",),
|
|
85
147
|
key="reset",
|
|
86
148
|
icon=WBIcon.REGENERATE.icon,
|
|
87
149
|
label="Reset Orders",
|
|
@@ -96,12 +158,12 @@ class OrderProposalButtonConfig(ButtonViewConfig):
|
|
|
96
158
|
),
|
|
97
159
|
bt.ActionButton(
|
|
98
160
|
method=RequestType.PATCH,
|
|
99
|
-
identifiers=("wbportfolio:
|
|
161
|
+
identifiers=("wbportfolio:order",),
|
|
100
162
|
key="normalize",
|
|
101
163
|
icon=WBIcon.EDIT.icon,
|
|
102
164
|
label="Normalize Orders",
|
|
103
165
|
description_fields="""
|
|
104
|
-
<p>Make sure all orders normalize to a total target weight of (
|
|
166
|
+
<p>Make sure all orders normalize to a total target weight of (1 - {{total_cash_weight}})</p>
|
|
105
167
|
""",
|
|
106
168
|
action_label="Normalize Orders",
|
|
107
169
|
title="Normalize Orders",
|
|
@@ -110,15 +172,12 @@ class OrderProposalButtonConfig(ButtonViewConfig):
|
|
|
110
172
|
),
|
|
111
173
|
bt.ActionButton(
|
|
112
174
|
method=RequestType.PATCH,
|
|
113
|
-
identifiers=("wbportfolio:
|
|
114
|
-
key="
|
|
115
|
-
icon=WBIcon.
|
|
116
|
-
label="
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
""",
|
|
120
|
-
action_label="Delete All Orders",
|
|
121
|
-
title="Delete All Orders",
|
|
175
|
+
identifiers=("wbportfolio:order",),
|
|
176
|
+
key="refresh_return",
|
|
177
|
+
icon=WBIcon.REFRESH.icon,
|
|
178
|
+
label="Refresh Returns & Price",
|
|
179
|
+
action_label="Refresh Returns & Price",
|
|
180
|
+
title="Refresh Returns & Price",
|
|
122
181
|
),
|
|
123
182
|
),
|
|
124
183
|
)
|
|
@@ -1,5 +1,86 @@
|
|
|
1
|
+
from wbcore import serializers as wb_serializers
|
|
2
|
+
from wbcore.contrib.icons import WBIcon
|
|
3
|
+
from wbcore.enums import RequestType
|
|
4
|
+
from wbcore.metadata.configs import buttons as bt
|
|
1
5
|
from wbcore.metadata.configs.buttons.enums import Button
|
|
2
6
|
from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
7
|
+
from wbcore.metadata.configs.display import create_simple_display
|
|
8
|
+
|
|
9
|
+
from wbportfolio.order_routing import ExecutionInstruction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExecutionInstructionSerializer(wb_serializers.Serializer):
|
|
13
|
+
execution_instruction = wb_serializers.ChoiceField(
|
|
14
|
+
choices=ExecutionInstruction.choices,
|
|
15
|
+
default=ExecutionInstruction.MARKET_ON_CLOSE.value,
|
|
16
|
+
clear_dependent_fields=False,
|
|
17
|
+
)
|
|
18
|
+
apply_execution_instruction_to_all_orders = wb_serializers.BooleanField(
|
|
19
|
+
default=False, label="Apply Execution instruction to all orders"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
percentage = wb_serializers.FloatField(
|
|
23
|
+
label="Percentage [1% - 25%]",
|
|
24
|
+
percent=True,
|
|
25
|
+
required=False,
|
|
26
|
+
allow_null=True,
|
|
27
|
+
on_unsatisfied_deps="hide",
|
|
28
|
+
depends_on=[
|
|
29
|
+
{
|
|
30
|
+
"field": "execution_instruction",
|
|
31
|
+
"options": {"activates_on": [ExecutionInstruction.IN_LINE_WITH_VOLUME.value]},
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
price = wb_serializers.FloatField(
|
|
36
|
+
label="Price (optional)",
|
|
37
|
+
required=False,
|
|
38
|
+
allow_null=True,
|
|
39
|
+
on_unsatisfied_deps="hide",
|
|
40
|
+
depends_on=[
|
|
41
|
+
{
|
|
42
|
+
"field": "execution_instruction",
|
|
43
|
+
"options": {"activates_on": [ExecutionInstruction.LIMIT_ORDER.value]},
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
good_for_date = wb_serializers.DateField(
|
|
48
|
+
label="Good For Date (optional)",
|
|
49
|
+
required=False,
|
|
50
|
+
allow_null=True,
|
|
51
|
+
on_unsatisfied_deps="hide",
|
|
52
|
+
depends_on=[
|
|
53
|
+
{
|
|
54
|
+
"field": "execution_instruction",
|
|
55
|
+
"options": {"activates_on": [ExecutionInstruction.LIMIT_ORDER.value]},
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
period = wb_serializers.IntegerField(
|
|
60
|
+
label="Period in minutes",
|
|
61
|
+
required=False,
|
|
62
|
+
allow_null=True,
|
|
63
|
+
on_unsatisfied_deps="hide",
|
|
64
|
+
depends_on=[
|
|
65
|
+
{
|
|
66
|
+
"field": "execution_instruction",
|
|
67
|
+
"options": {"activates_on": [ExecutionInstruction.VWAP.value, ExecutionInstruction.TWAP.value]},
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
time = wb_serializers.TimeField(
|
|
72
|
+
label="Time (UTC)",
|
|
73
|
+
format="%H:%M",
|
|
74
|
+
required=False,
|
|
75
|
+
allow_null=True,
|
|
76
|
+
on_unsatisfied_deps="hide",
|
|
77
|
+
depends_on=[
|
|
78
|
+
{
|
|
79
|
+
"field": "execution_instruction",
|
|
80
|
+
"options": {"activates_on": [ExecutionInstruction.VWAP.value, ExecutionInstruction.TWAP.value]},
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
)
|
|
3
84
|
|
|
4
85
|
|
|
5
86
|
class OrderOrderProposalButtonConfig(ButtonViewConfig):
|
|
@@ -7,3 +88,26 @@ class OrderOrderProposalButtonConfig(ButtonViewConfig):
|
|
|
7
88
|
return {
|
|
8
89
|
Button.SAVE_AND_CLOSE.value,
|
|
9
90
|
}
|
|
91
|
+
|
|
92
|
+
def get_custom_list_instance_buttons(self) -> set:
|
|
93
|
+
return {
|
|
94
|
+
bt.ActionButton(
|
|
95
|
+
method=RequestType.PUT,
|
|
96
|
+
identifiers=("wbportfolio:order",),
|
|
97
|
+
icon=WBIcon.DEAL_MONEY.icon,
|
|
98
|
+
key="execution_instruction",
|
|
99
|
+
label="Change Execution",
|
|
100
|
+
action_label="Execution changed",
|
|
101
|
+
description_fields="<p>Change Execution</p>",
|
|
102
|
+
serializer=ExecutionInstructionSerializer,
|
|
103
|
+
instance_display=create_simple_display(
|
|
104
|
+
[
|
|
105
|
+
["execution_instruction", "execution_instruction"],
|
|
106
|
+
["percentage", "percentage"],
|
|
107
|
+
["price", "good_for_date"],
|
|
108
|
+
["period", "time"],
|
|
109
|
+
["apply_execution_instruction_to_all_orders", "."],
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
}
|