wbportfolio 1.55.9__py2.py3-none-any.whl → 1.55.10rc0__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.
- wbportfolio/api_clients/ubs.py +11 -9
- wbportfolio/contrib/company_portfolio/configs/display.py +4 -4
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/import_export/handlers/asset_position.py +3 -3
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +3 -3
- 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 +2 -2
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/utils.py +6 -2
- 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/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/models/asset.py +2 -1
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/orders/order_proposals.py +2 -2
- wbportfolio/models/portfolio.py +7 -7
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +3 -0
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/pms/typing.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- wbportfolio/risk_management/backends/controversy_portfolio.py +1 -1
- wbportfolio/risk_management/backends/instrument_list_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/transactions/claim.py +2 -2
- wbportfolio/tests/models/test_portfolios.py +5 -5
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/viewsets/charts/assets.py +6 -4
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/RECORD +56 -56
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,21 +3,21 @@ import plotly.graph_objects as go
|
|
|
3
3
|
from networkx.drawing.nx_agraph import graphviz_layout
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def reformat_graph_layout(
|
|
6
|
+
def reformat_graph_layout(g, layout):
|
|
7
7
|
"""
|
|
8
8
|
this method provide positions based on layout algorithm
|
|
9
|
-
:param
|
|
9
|
+
:param g:
|
|
10
10
|
:param layout:
|
|
11
11
|
:return:
|
|
12
12
|
"""
|
|
13
13
|
if layout == "graphviz":
|
|
14
|
-
positions = graphviz_layout(
|
|
14
|
+
positions = graphviz_layout(g)
|
|
15
15
|
elif layout == "spring":
|
|
16
|
-
positions = nx.fruchterman_reingold_layout(
|
|
16
|
+
positions = nx.fruchterman_reingold_layout(g, k=0.5, iterations=1000)
|
|
17
17
|
elif layout == "spectral":
|
|
18
|
-
positions = nx.spectral_layout(
|
|
18
|
+
positions = nx.spectral_layout(g, scale=0.1)
|
|
19
19
|
elif layout == "random":
|
|
20
|
-
positions = nx.random_layout(
|
|
20
|
+
positions = nx.random_layout(g)
|
|
21
21
|
else:
|
|
22
22
|
raise Exception("please specify the layout from graphviz, spring, spectral or random")
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ def reformat_graph_layout(G, layout):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def networkx_graph_to_plotly(
|
|
28
|
-
|
|
28
|
+
g: nx.Graph,
|
|
29
29
|
labels: dict[str, str] | None = None,
|
|
30
30
|
node_size: int = 10,
|
|
31
31
|
edge_weight: int = 1,
|
|
@@ -36,12 +36,12 @@ def networkx_graph_to_plotly(
|
|
|
36
36
|
"""
|
|
37
37
|
Visualize a NetworkX graph using Plotly.
|
|
38
38
|
"""
|
|
39
|
-
positions = reformat_graph_layout(
|
|
39
|
+
positions = reformat_graph_layout(g, layout)
|
|
40
40
|
if not labels:
|
|
41
41
|
labels = {}
|
|
42
42
|
# Initialize edge traces
|
|
43
43
|
edge_traces = []
|
|
44
|
-
for edge in
|
|
44
|
+
for edge in g.edges():
|
|
45
45
|
x0, y0 = positions[edge[0]]
|
|
46
46
|
x1, y1 = positions[edge[1]]
|
|
47
47
|
|
|
@@ -52,12 +52,12 @@ def networkx_graph_to_plotly(
|
|
|
52
52
|
|
|
53
53
|
# Initialize node trace
|
|
54
54
|
node_x, node_y, node_colors, node_labels = [], [], [], []
|
|
55
|
-
for node in
|
|
55
|
+
for node in g.nodes():
|
|
56
56
|
x, y = positions[node]
|
|
57
57
|
node_x.append(x)
|
|
58
58
|
node_y.append(y)
|
|
59
59
|
node_labels.append(labels.get(node, node))
|
|
60
|
-
node_colors.append(len(list(
|
|
60
|
+
node_colors.append(len(list(g.neighbors(node)))) # Color based on degree
|
|
61
61
|
|
|
62
62
|
node_trace = go.Scatter(
|
|
63
63
|
x=node_x,
|
|
@@ -675,7 +675,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
675
675
|
except (ValidationError, DatabaseError) as e:
|
|
676
676
|
self.status = OrderProposal.Status.FAILED
|
|
677
677
|
if not silent_exception:
|
|
678
|
-
raise ValidationError(e)
|
|
678
|
+
raise ValidationError(e) from e
|
|
679
679
|
return
|
|
680
680
|
logger.info("Submitting order proposal ...")
|
|
681
681
|
self.submit()
|
|
@@ -1193,7 +1193,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
1193
1193
|
return warning
|
|
1194
1194
|
|
|
1195
1195
|
def can_cancelexecution(self):
|
|
1196
|
-
if
|
|
1196
|
+
if self.execution_status not in [ExecutionStatus.PENDING, ExecutionStatus.IN_DRAFT]:
|
|
1197
1197
|
return {"execution_status": "Execution can only be cancelled if it is not already executed"}
|
|
1198
1198
|
|
|
1199
1199
|
def update_execution_status(self):
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -72,7 +72,7 @@ class DefaultPortfolioQueryset(QuerySet):
|
|
|
72
72
|
"""
|
|
73
73
|
A method to sort the given queryset to return undependable portfolio first. This is very useful if a routine needs to be applied sequentially on portfolios by order of dependence.
|
|
74
74
|
"""
|
|
75
|
-
|
|
75
|
+
max_iterations: int = (
|
|
76
76
|
5 # in order to avoid circular dependency and infinite loop, we need to stop recursion at a max depth
|
|
77
77
|
)
|
|
78
78
|
remaining_portfolios = set(self)
|
|
@@ -85,7 +85,7 @@ class DefaultPortfolioQueryset(QuerySet):
|
|
|
85
85
|
dependency_relationships = PortfolioPortfolioThroughModel.objects.filter(
|
|
86
86
|
portfolio=p, dependency_portfolio__in=remaining_portfolios
|
|
87
87
|
) # get dependency portfolios
|
|
88
|
-
if iterator_counter >=
|
|
88
|
+
if iterator_counter >= max_iterations or (
|
|
89
89
|
not dependency_relationships.exists() and not bool(parent_portfolios)
|
|
90
90
|
): # if not dependency portfolio or parent portfolio that remained, then we yield
|
|
91
91
|
remaining_portfolios.remove(p)
|
|
@@ -949,7 +949,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
949
949
|
except IndexError:
|
|
950
950
|
position.portfolio_created = None
|
|
951
951
|
|
|
952
|
-
|
|
952
|
+
position.path = path
|
|
953
953
|
position.initial_shares = None
|
|
954
954
|
if portfolio_total_asset_value and (price_fx_portfolio := position.price * position.currency_fx_rate):
|
|
955
955
|
position.initial_shares = (position.weighting * portfolio_total_asset_value) / price_fx_portfolio
|
|
@@ -982,13 +982,13 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
982
982
|
)
|
|
983
983
|
if not to_date:
|
|
984
984
|
to_date = from_date
|
|
985
|
-
for
|
|
986
|
-
logger.info(f"Compute Look-Through for {self} at {
|
|
985
|
+
for val_date in pd.date_range(from_date, to_date, freq="B").date:
|
|
986
|
+
logger.info(f"Compute Look-Through for {self} at {val_date}")
|
|
987
987
|
portfolio_total_asset_value = (
|
|
988
|
-
self.primary_portfolio.get_total_asset_under_management(
|
|
988
|
+
self.primary_portfolio.get_total_asset_under_management(val_date) if not self.only_weighting else None
|
|
989
989
|
)
|
|
990
990
|
self.builder.add(
|
|
991
|
-
list(self.primary_portfolio.get_lookthrough_positions(
|
|
991
|
+
list(self.primary_portfolio.get_lookthrough_positions(val_date, portfolio_total_asset_value)),
|
|
992
992
|
infer_underlying_quote_price=True,
|
|
993
993
|
)
|
|
994
994
|
self.builder.bulk_create_positions(delete_leftovers=True)
|
|
@@ -61,6 +61,9 @@ class InstrumentPortfolioThroughModel(models.Model):
|
|
|
61
61
|
models.UniqueConstraint(fields=["instrument", "portfolio"], name="unique_portfolio_relationship"),
|
|
62
62
|
]
|
|
63
63
|
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"{self.instrument} - {self.portfolio}"
|
|
66
|
+
|
|
64
67
|
@classmethod
|
|
65
68
|
def get_portfolio(cls, instrument):
|
|
66
69
|
with suppress(InstrumentPortfolioThroughModel.DoesNotExist):
|
|
@@ -99,6 +102,9 @@ class PortfolioInstrumentPreferredClassificationThroughModel(models.Model):
|
|
|
99
102
|
related_name="preferred_classification_group_throughs",
|
|
100
103
|
)
|
|
101
104
|
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
return f"{self.portfolio} - {self.instrument}: ({self.classification})"
|
|
107
|
+
|
|
102
108
|
def save(self, *args, **kwargs) -> None:
|
|
103
109
|
if not self.classification_group and self.classification:
|
|
104
110
|
self.classification_group = self.classification.group
|
wbportfolio/models/products.py
CHANGED
|
@@ -89,6 +89,9 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
89
89
|
help_text=_("The Evaluation Frequency in RRULE format"),
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
return f"{self.portfolio.name} ({self.rebalancing_model})"
|
|
94
|
+
|
|
92
95
|
def save(self, *args, **kwargs):
|
|
93
96
|
if not self.activation_date:
|
|
94
97
|
try:
|
wbportfolio/models/roles.py
CHANGED
|
@@ -52,16 +52,10 @@ class PortfolioRole(models.Model):
|
|
|
52
52
|
return f"{self.role_type} {self.person.computed_str}"
|
|
53
53
|
|
|
54
54
|
def save(self, *args, **kwargs):
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
self.RoleType.ANALYST,
|
|
60
|
-
], self.default_error_messages["manager"].format(model="instrument")
|
|
61
|
-
|
|
62
|
-
assert (self.start and self.end and self.start < self.end) or (
|
|
63
|
-
not self.start or not self.end
|
|
64
|
-
), self.default_error_messages["start_end"]
|
|
55
|
+
if self.role_type in [self.RoleType.MANAGER, self.RoleType.RISK_MANAGER] and self.instrument:
|
|
56
|
+
raise ValueError(self.default_error_messages["manager"].format(model="instrument"))
|
|
57
|
+
if self.start and self.end and self.start > self.end:
|
|
58
|
+
raise ValueError(self.default_error_messages["start_end"])
|
|
65
59
|
|
|
66
60
|
super().save(*args, **kwargs)
|
|
67
61
|
|
|
@@ -259,9 +259,10 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
259
259
|
return f"{self.reference_id} {self.product.name} ({self.bank} - {self.shares:,} shares - {self.date}) "
|
|
260
260
|
|
|
261
261
|
def save(self, *args, auto_match: bool = True, **kwargs):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
if self.shares is None and self.nominal_amount is None:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
|
|
265
|
+
)
|
|
265
266
|
if self.product:
|
|
266
267
|
if self.shares is not None:
|
|
267
268
|
self.nominal_amount = self.shares * self.product.share_price
|
|
@@ -447,7 +448,7 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
447
448
|
return self.can_approve()
|
|
448
449
|
|
|
449
450
|
def auto_match(self) -> Trade | None:
|
|
450
|
-
|
|
451
|
+
shares_epsilon = 1 # share
|
|
451
452
|
auto_match_trade = None
|
|
452
453
|
# Obvious filtering
|
|
453
454
|
trades = Trade.valid_customer_trade_objects.filter(
|
|
@@ -458,7 +459,7 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
458
459
|
trades = trades.filter(underlying_instrument=self.product)
|
|
459
460
|
# Find trades by shares (or remaining to be claimed)
|
|
460
461
|
trades = trades.filter(
|
|
461
|
-
Q(diff_shares__lte=self.shares +
|
|
462
|
+
Q(diff_shares__lte=self.shares + shares_epsilon) & Q(diff_shares__gte=self.shares - shares_epsilon)
|
|
462
463
|
)
|
|
463
464
|
if trades.count() == 1:
|
|
464
465
|
auto_match_trade = trades.first()
|
wbportfolio/pms/typing.py
CHANGED
|
@@ -269,7 +269,7 @@ class TradeBatch:
|
|
|
269
269
|
|
|
270
270
|
def convert_to_portfolio(self, use_effective: bool = False, *extra_positions):
|
|
271
271
|
positions = []
|
|
272
|
-
for
|
|
272
|
+
for trade in self.trades_map.values():
|
|
273
273
|
positions.append(
|
|
274
274
|
Position(
|
|
275
275
|
underlying_instrument=trade.underlying_instrument,
|
|
@@ -108,11 +108,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
108
108
|
return df.any()
|
|
109
109
|
else:
|
|
110
110
|
if missing_exchanges.exists():
|
|
111
|
-
|
|
112
|
-
self,
|
|
113
|
-
"_validation_errors",
|
|
114
|
-
f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}",
|
|
115
|
-
)
|
|
111
|
+
self._validation_errors = f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}"
|
|
116
112
|
return df.all()
|
|
117
113
|
return False
|
|
118
114
|
|
|
@@ -44,7 +44,7 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
|
44
44
|
return RuleBackendSerializer
|
|
45
45
|
|
|
46
46
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
47
|
-
for instrument_id
|
|
47
|
+
for instrument_id in portfolio.positions_map.keys():
|
|
48
48
|
instrument = Instrument.objects.get(id=instrument_id)
|
|
49
49
|
if (
|
|
50
50
|
controversies := Controversy.objects.filter(
|
|
@@ -64,7 +64,7 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
|
64
64
|
return RuleBackendSerializer
|
|
65
65
|
|
|
66
66
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
67
|
-
for instrument_id
|
|
67
|
+
for instrument_id in portfolio.positions_map.keys():
|
|
68
68
|
instrument = Instrument.objects.get(id=instrument_id)
|
|
69
69
|
relationships = self.instruments_relationship.filter(instrument=instrument, validated=True)
|
|
70
70
|
|
|
@@ -92,7 +92,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
|
|
|
92
92
|
date=weekday, net_value=500, calculated=False, instrument=benchmark
|
|
93
93
|
)
|
|
94
94
|
RelatedInstrumentThroughModel.objects.create(instrument=product, related_instrument=benchmark, is_primary=True)
|
|
95
|
-
|
|
95
|
+
stop_loss_instrument_backend.dynamic_benchmark_type = "PRIMARY_BENCHMARK"
|
|
96
96
|
|
|
97
97
|
res = list(stop_loss_instrument_backend.check_rule())
|
|
98
98
|
assert len(res) == 0
|
|
@@ -105,7 +105,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
|
|
|
105
105
|
assert len(res) == 1
|
|
106
106
|
assert res[0].breached_object.id == product.id
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
stop_loss_instrument_backend.static_benchmark = benchmark
|
|
109
109
|
res = list(stop_loss_instrument_backend.check_rule())
|
|
110
110
|
assert len(res) == 1
|
|
111
111
|
assert res[0].breached_object.id == product.id
|
|
@@ -119,7 +119,7 @@ class TestStopLossPortfolioRuleModel(PortfolioTestMixin):
|
|
|
119
119
|
res = list(stop_loss_portfolio_backend.check_rule())
|
|
120
120
|
assert len(res) == 0
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
stop_loss_portfolio_backend.static_benchmark = benchmark
|
|
123
123
|
res = list(stop_loss_portfolio_backend.check_rule())
|
|
124
124
|
assert len(res) == 1
|
|
125
125
|
assert res[0].breached_object.id == instrument.id
|
|
@@ -49,8 +49,8 @@ class ClaimAPIModelSerializer(serializers.ModelSerializer):
|
|
|
49
49
|
if "isin" in request.data:
|
|
50
50
|
try:
|
|
51
51
|
data["product"] = Product.objects.get(isin=request.data["isin"])
|
|
52
|
-
except Product.DoesNotExist:
|
|
53
|
-
raise ValidationError({"isin": "A product with this ISIN does not exist."})
|
|
52
|
+
except Product.DoesNotExist as e:
|
|
53
|
+
raise ValidationError({"isin": "A product with this ISIN does not exist."}) from e
|
|
54
54
|
if trade := data.get("trade", None):
|
|
55
55
|
if not trade.is_claimable:
|
|
56
56
|
raise ValidationError({"trade": "Only a claimable trade can be selected"})
|
|
@@ -924,13 +924,13 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
924
924
|
|
|
925
925
|
analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
|
|
926
926
|
assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
|
|
927
|
-
|
|
927
|
+
expected_x = pd.DataFrame(
|
|
928
928
|
[[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
|
|
929
929
|
columns=[i1.id, i2.id],
|
|
930
930
|
index=[(weekday + BDay(1)).date()],
|
|
931
931
|
)
|
|
932
|
-
|
|
933
|
-
pd.testing.assert_frame_equal(analytic_portfolio.X,
|
|
932
|
+
expected_x.index = pd.to_datetime(expected_x.index)
|
|
933
|
+
pd.testing.assert_frame_equal(analytic_portfolio.X, expected_x, check_names=False, check_freq=False)
|
|
934
934
|
|
|
935
935
|
def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
|
|
936
936
|
a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
|
|
@@ -1010,7 +1010,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1010
1010
|
assert len(res) == 1
|
|
1011
1011
|
assert a.date == weekday
|
|
1012
1012
|
assert a.underlying_quote == instrument
|
|
1013
|
-
assert a.underlying_quote_price
|
|
1013
|
+
assert a.underlying_quote_price is None
|
|
1014
1014
|
assert a.initial_price == p.net_value
|
|
1015
1015
|
assert a.weighting == pytest.approx(weights[instrument.id], abs=Decimal(10e-6))
|
|
1016
1016
|
assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
|
|
@@ -1053,7 +1053,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1053
1053
|
assert next(gen)[0] == middle_date, "Drifting weight with a non automatic rebalancer stops the iteration"
|
|
1054
1054
|
try:
|
|
1055
1055
|
next(gen)
|
|
1056
|
-
|
|
1056
|
+
raise AssertionError("the next iteration should stop and return the rebalancing")
|
|
1057
1057
|
except StopIteration as e:
|
|
1058
1058
|
rebalancing_order_proposal = e.value
|
|
1059
1059
|
assert rebalancing_order_proposal.trade_date == rebalancing_date
|
|
@@ -14,10 +14,6 @@ fake = Faker()
|
|
|
14
14
|
|
|
15
15
|
@pytest.mark.django_db
|
|
16
16
|
class TestAdjustmentModel:
|
|
17
|
-
@pytest.fixture()
|
|
18
|
-
def applied_adjustment(self):
|
|
19
|
-
return AdjustmentFactory.create(status=Adjustment.Status.APPLIED)
|
|
20
|
-
|
|
21
17
|
@pytest.fixture()
|
|
22
18
|
def old_adjustment(self):
|
|
23
19
|
return AdjustmentFactory.create(status=Adjustment.Status.PENDING, date=fake.past_date())
|
|
@@ -205,7 +201,7 @@ class TestAdjustmentModel:
|
|
|
205
201
|
post_adjustment_on_prices(adjustment.id)
|
|
206
202
|
a1.refresh_from_db()
|
|
207
203
|
adjustment.refresh_from_db()
|
|
208
|
-
a1.applied_adjustment == adjustment
|
|
204
|
+
assert a1.applied_adjustment == adjustment
|
|
209
205
|
assert adjustment.status == Adjustment.Status.APPLIED
|
|
210
206
|
|
|
211
207
|
@patch("wbportfolio.models.adjustments.send_notification")
|
|
@@ -230,5 +226,4 @@ class TestAdjustmentModel:
|
|
|
230
226
|
mock_check_fct.return_value = False
|
|
231
227
|
post_adjustment_on_prices(adjustment.id)
|
|
232
228
|
adjustment.refresh_from_db()
|
|
233
|
-
mock_delay_fct.call_args[0] == user_porftolio_manager.id
|
|
234
229
|
assert adjustment.status == Adjustment.Status.PENDING
|
|
@@ -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}
|
|
@@ -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"
|
|
@@ -169,8 +171,8 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
169
171
|
dict(Classification.objects.filter(id__in=df["classification"].dropna()).values_list("id", "name"))
|
|
170
172
|
)
|
|
171
173
|
elif self.group_by == AssetPositionGroupBy.CASH:
|
|
172
|
-
df.loc[df["id"]
|
|
173
|
-
df.loc[df["id"]
|
|
174
|
+
df.loc[df["id"], "label"] = "Cash"
|
|
175
|
+
df.loc[~df["id"], "label"] = "Non-Cash"
|
|
174
176
|
elif self.group_by == AssetPositionGroupBy.COUNTRY:
|
|
175
177
|
df["label"] = df["id"].map(dict(Geography.objects.filter(id__in=df["id"]).values_list("id", "name")))
|
|
176
178
|
elif self.group_by == AssetPositionGroupBy.CURRENCY:
|
|
@@ -307,7 +309,7 @@ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewset
|
|
|
307
309
|
|
|
308
310
|
text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
|
|
309
311
|
text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
|
|
310
|
-
|
|
312
|
+
self.nb_rows = df.shape[0]
|
|
311
313
|
fig.add_trace(
|
|
312
314
|
go.Bar(
|
|
313
315
|
y=df.instrument_id,
|
|
@@ -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,
|
|
@@ -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
|
),
|