wbportfolio 1.49.9__py2.py3-none-any.whl → 1.49.11__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/import_export/parsers/natixis/fees.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +31 -17
- wbportfolio/models/asset.py +154 -0
- wbportfolio/models/portfolio.py +205 -255
- wbportfolio/models/portfolio_cash_flow.py +5 -5
- wbportfolio/models/roles.py +1 -5
- wbportfolio/models/transactions/trade_proposals.py +18 -4
- wbportfolio/models/transactions/trades.py +1 -1
- wbportfolio/tests/models/test_portfolios.py +50 -65
- wbportfolio/tests/models/test_products.py +0 -5
- wbportfolio/tests/models/transactions/test_trade_proposals.py +17 -5
- wbportfolio/viewsets/portfolios.py +2 -2
- {wbportfolio-1.49.9.dist-info → wbportfolio-1.49.11.dist-info}/METADATA +1 -1
- {wbportfolio-1.49.9.dist-info → wbportfolio-1.49.11.dist-info}/RECORD +16 -16
- {wbportfolio-1.49.9.dist-info → wbportfolio-1.49.11.dist-info}/WHEEL +0 -0
- {wbportfolio-1.49.9.dist-info → wbportfolio-1.49.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -76,11 +76,11 @@ class DailyPortfolioCashFlow(ImportMixin, WBModel):
|
|
|
76
76
|
|
|
77
77
|
with suppress(self.DoesNotExist):
|
|
78
78
|
if self.total_assets is None or self.pending:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
prev = self.portfolio.daily_cashflows.filter(value_date__lt=self.value_date).latest("value_date")
|
|
80
|
+
if prev.pending:
|
|
81
|
+
self.total_assets = prev.estimated_total_assets
|
|
82
|
+
else:
|
|
83
|
+
self.total_assets = prev.total_assets
|
|
84
84
|
|
|
85
85
|
if self.total_assets is None:
|
|
86
86
|
self.total_assets = 0
|
wbportfolio/models/roles.py
CHANGED
|
@@ -57,11 +57,7 @@ class PortfolioRole(models.Model):
|
|
|
57
57
|
) or self.role_type in [
|
|
58
58
|
self.RoleType.PORTFOLIO_MANAGER,
|
|
59
59
|
self.RoleType.ANALYST,
|
|
60
|
-
], self.default_error_messages[
|
|
61
|
-
"manager"
|
|
62
|
-
].format(
|
|
63
|
-
model="instrument"
|
|
64
|
-
)
|
|
60
|
+
], self.default_error_messages["manager"].format(model="instrument")
|
|
65
61
|
|
|
66
62
|
assert (self.start and self.end and self.start < self.end) or (
|
|
67
63
|
not self.start or not self.end
|
|
@@ -26,7 +26,7 @@ from wbportfolio.pms.trading import TradingService
|
|
|
26
26
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
27
27
|
from wbportfolio.pms.typing import TradeBatch as TradeBatchDTO
|
|
28
28
|
|
|
29
|
-
from .. import AssetPosition
|
|
29
|
+
from ..asset import AssetPosition, AssetPositionIterator
|
|
30
30
|
from .trades import Trade
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger("pms")
|
|
@@ -80,6 +80,13 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
80
80
|
def save(self, *args, **kwargs):
|
|
81
81
|
if not self.trade_date and self.portfolio.assets.exists():
|
|
82
82
|
self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
83
|
+
|
|
84
|
+
# if a trade proposal is created before the existing earliest trade proposal, we automatically shift the linked instruments inception date to allow automatic NAV computation since the new inception date
|
|
85
|
+
if not self.portfolio.trade_proposals.filter(trade_date__lt=self.trade_date).exists():
|
|
86
|
+
new_inception_date = (self.trade_date + BDay(1)).date()
|
|
87
|
+
self.portfolio.instruments.filter(inception_date__gt=new_inception_date).update(
|
|
88
|
+
inception_date=new_inception_date
|
|
89
|
+
)
|
|
83
90
|
super().save(*args, **kwargs)
|
|
84
91
|
|
|
85
92
|
@property
|
|
@@ -302,9 +309,12 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
302
309
|
next_trade_date = (
|
|
303
310
|
next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
|
|
304
311
|
)
|
|
305
|
-
overriding_trade_proposal =
|
|
312
|
+
positions, overriding_trade_proposal = self.portfolio.drift_weights(
|
|
306
313
|
last_trade_proposal.trade_date, next_trade_date
|
|
307
314
|
)
|
|
315
|
+
self.portfolio.bulk_create_positions(
|
|
316
|
+
positions, delete_leftovers=True, compute_metrics=False, evaluate_rebalancer=False
|
|
317
|
+
)
|
|
308
318
|
if overriding_trade_proposal:
|
|
309
319
|
last_trade_proposal_created = True
|
|
310
320
|
last_trade_proposal = overriding_trade_proposal
|
|
@@ -527,7 +537,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
527
537
|
assets.append(estimated_cash_position)
|
|
528
538
|
|
|
529
539
|
Trade.objects.bulk_update(trades, ["status"])
|
|
530
|
-
self.portfolio.bulk_create_positions(
|
|
540
|
+
self.portfolio.bulk_create_positions(
|
|
541
|
+
AssetPositionIterator(self.portfolio).add(assets), evaluate_rebalancer=False, force_save=True
|
|
542
|
+
)
|
|
531
543
|
if replay and self.portfolio.is_manageable:
|
|
532
544
|
replay_as_task.delay(self.id)
|
|
533
545
|
|
|
@@ -647,7 +659,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
647
659
|
asset.set_weighting(asset.weighting - trade.weighting)
|
|
648
660
|
assets.append(asset)
|
|
649
661
|
Trade.objects.bulk_update(trades, ["status"])
|
|
650
|
-
self.portfolio.bulk_create_positions(
|
|
662
|
+
self.portfolio.bulk_create_positions(
|
|
663
|
+
AssetPositionIterator(self.portfolio).add(assets), evaluate_rebalancer=False, force_save=True
|
|
664
|
+
)
|
|
651
665
|
|
|
652
666
|
def can_revert(self):
|
|
653
667
|
errors = dict()
|
|
@@ -443,7 +443,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
443
443
|
elif (
|
|
444
444
|
assets := AssetPosition.unannotated_objects.filter(
|
|
445
445
|
underlying_quote=self.underlying_instrument,
|
|
446
|
-
|
|
446
|
+
date__lte=self.value_date,
|
|
447
447
|
portfolio=self.portfolio,
|
|
448
448
|
)
|
|
449
449
|
).exists():
|
|
@@ -20,8 +20,9 @@ from wbportfolio.models import (
|
|
|
20
20
|
PortfolioPortfolioThroughModel,
|
|
21
21
|
Trade,
|
|
22
22
|
)
|
|
23
|
+
from wbportfolio.models.asset import AssetPositionIterator
|
|
23
24
|
|
|
24
|
-
from ...models.portfolio import
|
|
25
|
+
from ...models.portfolio import get_returns, update_portfolio_after_investable_universe
|
|
25
26
|
from .utils import PortfolioTestMixin
|
|
26
27
|
|
|
27
28
|
fake = Faker()
|
|
@@ -266,17 +267,15 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
266
267
|
for pos in AssetPosition.objects.all():
|
|
267
268
|
assert float(pos.weighting) == pytest.approx(float(pos.total_value_fx_portfolio / total_value), rel=1e-2)
|
|
268
269
|
|
|
269
|
-
mock_estimate_net_asset_values.assert_called_once_with(portfolio, (weekday + BDay(1)).date())
|
|
270
|
+
mock_estimate_net_asset_values.assert_called_once_with(portfolio, (weekday + BDay(1)).date(), weights=None)
|
|
270
271
|
mock_compute_metrics.assert_called_once_with(
|
|
271
272
|
weekday, basket_id=portfolio.id, basket_content_type_id=ContentType.objects.get_for_model(Portfolio).id
|
|
272
273
|
)
|
|
273
274
|
|
|
274
|
-
@patch.object(Portfolio, "get_total_asset_under_management")
|
|
275
275
|
@patch.object(Portfolio, "compute_lookthrough", autospec=True)
|
|
276
276
|
def test_change_at_date_with_dependent_portfolio(
|
|
277
277
|
self,
|
|
278
278
|
mock_compute_lookthrough,
|
|
279
|
-
mock_get_total_asset_under_management,
|
|
280
279
|
portfolio_factory,
|
|
281
280
|
product_factory,
|
|
282
281
|
instrument_price_factory,
|
|
@@ -284,16 +283,12 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
284
283
|
weekday,
|
|
285
284
|
):
|
|
286
285
|
base_portfolio = portfolio_factory.create()
|
|
287
|
-
base_portfolio_total_asset_under_management = fake.pydecimal()
|
|
288
|
-
mock_get_total_asset_under_management.return_value = base_portfolio_total_asset_under_management
|
|
289
286
|
|
|
290
287
|
dependent_portfolio = portfolio_factory.create(is_lookthrough=True)
|
|
291
288
|
dependent_portfolio.depends_on.add(base_portfolio)
|
|
292
289
|
base_portfolio.change_at_date(weekday)
|
|
293
290
|
|
|
294
|
-
mock_compute_lookthrough.assert_called_once_with(
|
|
295
|
-
dependent_portfolio, weekday, portfolio_total_asset_value=base_portfolio_total_asset_under_management
|
|
296
|
-
)
|
|
291
|
+
mock_compute_lookthrough.assert_called_once_with(dependent_portfolio, weekday)
|
|
297
292
|
|
|
298
293
|
def test_is_active_at_date(
|
|
299
294
|
self,
|
|
@@ -437,14 +432,6 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
437
432
|
)
|
|
438
433
|
instrument_price_factory.create(date=next_day, instrument=instrument)
|
|
439
434
|
|
|
440
|
-
# asset_position_factory.create(
|
|
441
|
-
# portfolio=portfolio,
|
|
442
|
-
# date=next_day,
|
|
443
|
-
# underlying_instrument=instrument,
|
|
444
|
-
# currency=instrument.currency,
|
|
445
|
-
# exchange=a1.exchange,
|
|
446
|
-
# portfolio_created=a1.portfolio_created,
|
|
447
|
-
# )
|
|
448
435
|
active_product.delisted_date = weekday
|
|
449
436
|
active_product.save()
|
|
450
437
|
# Test1: test if unactive portfolio keep having the to date assets. (asset found at next day are suppose to be deleted when the portfolio is non active at the from date)
|
|
@@ -465,6 +452,8 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
465
452
|
initial_shares = a1.initial_shares
|
|
466
453
|
|
|
467
454
|
# Test that estimated shares keep being updated
|
|
455
|
+
portfolio.only_weighting = False
|
|
456
|
+
portfolio.save()
|
|
468
457
|
a1.initial_shares *= 2
|
|
469
458
|
a1.save()
|
|
470
459
|
portfolio.propagate_or_update_assets(weekday, next_day)
|
|
@@ -491,8 +480,9 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
491
480
|
a_future.is_estimated = True
|
|
492
481
|
a_future.save()
|
|
493
482
|
portfolio.propagate_or_update_assets(weekday, next_day)
|
|
494
|
-
|
|
495
|
-
|
|
483
|
+
with pytest.raises(AssetPosition.DoesNotExist):
|
|
484
|
+
AssetPosition.objects.get(portfolio=portfolio, date=next_day)
|
|
485
|
+
# assert a_future.weighting == 0
|
|
496
486
|
|
|
497
487
|
def test_update_preferred_classification_per_instrument(
|
|
498
488
|
self, portfolio, asset_position_factory, equity_factory, classification_factory, classification_group_factory
|
|
@@ -588,6 +578,11 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
588
578
|
|
|
589
579
|
portfolio.is_tracked = False
|
|
590
580
|
portfolio.save()
|
|
581
|
+
assert portfolio.is_manageable is True
|
|
582
|
+
assert Portfolio.tracked_objects.exists()
|
|
583
|
+
|
|
584
|
+
portfolio.is_manageable = False
|
|
585
|
+
portfolio.save()
|
|
591
586
|
assert not Portfolio.tracked_objects.exists()
|
|
592
587
|
|
|
593
588
|
def test_is_invested_at_date(self, portfolio_factory):
|
|
@@ -600,8 +595,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
600
595
|
assert set(Portfolio.objects.filter_invested_at_date(date(2024, 1, 2))) == {portfolio}
|
|
601
596
|
assert set(Portfolio.objects.filter_invested_at_date(date(2024, 1, 1))) == set()
|
|
602
597
|
|
|
598
|
+
@patch.object(Portfolio, "get_total_asset_under_management", autospec=True)
|
|
603
599
|
def test_compute_lookthrough(
|
|
604
600
|
self,
|
|
601
|
+
mock_fct,
|
|
605
602
|
active_product,
|
|
606
603
|
weekday,
|
|
607
604
|
portfolio_factory,
|
|
@@ -766,9 +763,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
766
763
|
)
|
|
767
764
|
assert Decimal(1.0) == pytest.approx(product_portfolio.assets.aggregate(s=Sum("weighting"))["s"])
|
|
768
765
|
|
|
769
|
-
product_portfolio.
|
|
766
|
+
product_portfolio.only_weighting = False
|
|
770
767
|
product_portfolio.save()
|
|
771
|
-
|
|
768
|
+
mock_fct.return_value = Decimal(1_000_000)
|
|
769
|
+
product_portfolio.compute_lookthrough(weekday)
|
|
772
770
|
position = product_portfolio.assets.get(
|
|
773
771
|
portfolio_created=index1_portfolio,
|
|
774
772
|
underlying_instrument=a1_1.underlying_instrument,
|
|
@@ -891,24 +889,20 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
891
889
|
assert set(portfolio.pms_instruments) == {product_group, product, index}
|
|
892
890
|
|
|
893
891
|
@pytest.mark.parametrize(
|
|
894
|
-
"
|
|
892
|
+
"portfolio__is_manageable, portfolio__is_lookthrough",
|
|
895
893
|
[
|
|
896
|
-
(True, True
|
|
897
|
-
(
|
|
898
|
-
(
|
|
899
|
-
(False, True, True),
|
|
900
|
-
(False, True, False),
|
|
901
|
-
(False, False, True),
|
|
902
|
-
(False, False, False),
|
|
894
|
+
(True, True),
|
|
895
|
+
(False, True),
|
|
896
|
+
(False, False),
|
|
903
897
|
],
|
|
904
898
|
)
|
|
905
899
|
def test_cannot_be_rebalanced(self, portfolio):
|
|
906
900
|
assert portfolio.can_be_rebalanced is False
|
|
907
901
|
|
|
908
902
|
@pytest.mark.parametrize(
|
|
909
|
-
"
|
|
903
|
+
"portfolio__is_manageable, portfolio__is_lookthrough",
|
|
910
904
|
[
|
|
911
|
-
(True,
|
|
905
|
+
(True, False),
|
|
912
906
|
],
|
|
913
907
|
)
|
|
914
908
|
def test_can_be_rebalanced(self, portfolio):
|
|
@@ -917,8 +911,8 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
917
911
|
def test_get_analytic_portfolio(
|
|
918
912
|
self, weekday, portfolio, asset_position_factory, instrument_factory, instrument_price_factory
|
|
919
913
|
):
|
|
920
|
-
i1 = instrument_factory.create()
|
|
921
|
-
i2 = instrument_factory.create()
|
|
914
|
+
i1 = instrument_factory.create(currency=portfolio.currency)
|
|
915
|
+
i2 = instrument_factory.create(currency=portfolio.currency)
|
|
922
916
|
p10 = instrument_price_factory.create(instrument=i1, date=weekday)
|
|
923
917
|
p11 = instrument_price_factory.create(instrument=i1, date=(weekday + BDay(1)).date())
|
|
924
918
|
p20 = instrument_price_factory.create(instrument=i2, date=weekday)
|
|
@@ -931,7 +925,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
931
925
|
)
|
|
932
926
|
a2.refresh_from_db()
|
|
933
927
|
|
|
934
|
-
analytic_portfolio
|
|
928
|
+
analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
|
|
935
929
|
assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
|
|
936
930
|
expected_X = pd.DataFrame(
|
|
937
931
|
[[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
|
|
@@ -968,7 +962,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
968
962
|
def test_update_portfolio_after_investable_universe(
|
|
969
963
|
self, mock_fct, weekday, portfolio_factory, asset_position_factory
|
|
970
964
|
):
|
|
971
|
-
untracked_portfolio = portfolio_factory.create(is_tracked=False) # noqa
|
|
965
|
+
untracked_portfolio = portfolio_factory.create(is_tracked=False, is_manageable=False) # noqa
|
|
972
966
|
asset_position_factory.create(portfolio=untracked_portfolio)
|
|
973
967
|
tracked_lookthrough_portfolio = portfolio_factory.create(is_tracked=True, is_lookthrough=True) # noqa
|
|
974
968
|
asset_position_factory.create(portfolio=tracked_lookthrough_portfolio)
|
|
@@ -1009,12 +1003,14 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1009
1003
|
p = instrument_price_factory.create(instrument=instrument, date=weekday)
|
|
1010
1004
|
fx_portfolio = currency_fx_rates_factory.create(currency=portfolio.currency, date=weekday)
|
|
1011
1005
|
fx_instrument = currency_fx_rates_factory.create(currency=instrument.currency, date=weekday)
|
|
1006
|
+
instrument_id: int = instrument.id
|
|
1007
|
+
weights = {instrument_id: random.random()}
|
|
1008
|
+
positions = AssetPositionIterator(
|
|
1009
|
+
portfolio, prices={weekday: {instrument_id: p.net_value}}, infer_underlying_quote_price=False
|
|
1010
|
+
)
|
|
1011
|
+
positions.add((weekday, weights))
|
|
1012
1012
|
|
|
1013
|
-
|
|
1014
|
-
prices = pd.DataFrame([{instrument.id: p.net_value, "date": weekday}]).set_index("date")
|
|
1015
|
-
prices.index = pd.to_datetime(prices.index)
|
|
1016
|
-
converter = PositionDictConverter(portfolio, prices)
|
|
1017
|
-
res = list(converter.convert({weekday: weights}))
|
|
1013
|
+
res = list(positions)
|
|
1018
1014
|
a = res[0]
|
|
1019
1015
|
assert len(res) == 1
|
|
1020
1016
|
assert a.date == weekday
|
|
@@ -1029,7 +1025,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1029
1025
|
a.save()
|
|
1030
1026
|
assert a
|
|
1031
1027
|
|
|
1032
|
-
def
|
|
1028
|
+
def test_drift_weights_with_rebalancer(
|
|
1033
1029
|
self,
|
|
1034
1030
|
weekday,
|
|
1035
1031
|
rebalancer_factory,
|
|
@@ -1056,15 +1052,12 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1056
1052
|
instrument_price_factory.create(instrument=i2, date=rebalancing_date)
|
|
1057
1053
|
|
|
1058
1054
|
rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
|
|
1059
|
-
rebalancing_trade_proposal = portfolio.
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
assert
|
|
1063
|
-
|
|
1064
|
-
with
|
|
1065
|
-
AssetPosition.DoesNotExist
|
|
1066
|
-
): # there is no asset position because the rebalancing stopped it:
|
|
1067
|
-
portfolio.assets.get(date=rebalancing_date)
|
|
1055
|
+
positions, rebalancing_trade_proposal = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date())
|
|
1056
|
+
assert rebalancing_trade_proposal.trade_date == rebalancing_date
|
|
1057
|
+
assert rebalancing_trade_proposal.status == "SUBMIT"
|
|
1058
|
+
assert set(positions.get_weights().keys()) == {
|
|
1059
|
+
middle_date,
|
|
1060
|
+
}, "Drifting weight with a non automatic rebalancer stops the iteration"
|
|
1068
1061
|
|
|
1069
1062
|
# we expect a equally rebalancing (default) so both trades needs to be created
|
|
1070
1063
|
t1 = rebalancing_trade_proposal.trades.get(transaction_date=rebalancing_date, underlying_instrument=i1)
|
|
@@ -1090,17 +1083,17 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1090
1083
|
a1 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i1)
|
|
1091
1084
|
|
|
1092
1085
|
# check initial creation
|
|
1093
|
-
portfolio.bulk_create_positions([a1])
|
|
1086
|
+
portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a1]))
|
|
1094
1087
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a1.weighting
|
|
1095
1088
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i1
|
|
1096
1089
|
|
|
1097
1090
|
# check that if we change key value, an already exising position will be updated accordingly
|
|
1098
1091
|
a1.weighting = Decimal(0.5)
|
|
1099
|
-
portfolio.bulk_create_positions([a1])
|
|
1092
|
+
portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a1]))
|
|
1100
1093
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == Decimal(0.5)
|
|
1101
1094
|
|
|
1102
1095
|
a2 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i2)
|
|
1103
|
-
portfolio.bulk_create_positions([a2])
|
|
1096
|
+
portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a2]))
|
|
1104
1097
|
assert (
|
|
1105
1098
|
AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1).weighting
|
|
1106
1099
|
== a1.weighting
|
|
@@ -1111,7 +1104,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1111
1104
|
)
|
|
1112
1105
|
|
|
1113
1106
|
a3 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i3)
|
|
1114
|
-
portfolio.bulk_create_positions([a3], delete_leftovers=True)
|
|
1107
|
+
portfolio.bulk_create_positions(AssetPositionIterator(portfolio).add([a3]), delete_leftovers=True)
|
|
1115
1108
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a3.weighting
|
|
1116
1109
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i3
|
|
1117
1110
|
|
|
@@ -1146,7 +1139,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1146
1139
|
i11.refresh_from_db()
|
|
1147
1140
|
i12.refresh_from_db()
|
|
1148
1141
|
i13.refresh_from_db()
|
|
1149
|
-
returns
|
|
1142
|
+
returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
|
|
1150
1143
|
|
|
1151
1144
|
expected_returns = pd.DataFrame(
|
|
1152
1145
|
[[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
|
|
@@ -1157,14 +1150,8 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1157
1150
|
expected_returns.index = pd.to_datetime(expected_returns.index)
|
|
1158
1151
|
pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
|
|
1159
1152
|
|
|
1160
|
-
@patch.object(Portfolio, "get_total_asset_under_management")
|
|
1161
1153
|
@patch.object(Portfolio, "compute_lookthrough", autospec=True)
|
|
1162
|
-
def test_handle_controlling_portfolio_change_at_date(
|
|
1163
|
-
self, mock_compute_lookthrough, mock_get_total_asset_under_management, weekday, portfolio_factory
|
|
1164
|
-
):
|
|
1165
|
-
portfolio_total_asset_value = Decimal(1_000_000)
|
|
1166
|
-
mock_get_total_asset_under_management.return_value = portfolio_total_asset_value
|
|
1167
|
-
|
|
1154
|
+
def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
|
|
1168
1155
|
primary_portfolio = portfolio_factory.create(only_weighting=True)
|
|
1169
1156
|
lookthrough_portfolio = portfolio_factory.create(is_lookthrough=True, only_weighting=False)
|
|
1170
1157
|
PortfolioPortfolioThroughModel.objects.create(
|
|
@@ -1174,6 +1161,4 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1174
1161
|
)
|
|
1175
1162
|
|
|
1176
1163
|
primary_portfolio.handle_controlling_portfolio_change_at_date(weekday)
|
|
1177
|
-
mock_compute_lookthrough.assert_called_once_with(
|
|
1178
|
-
lookthrough_portfolio, weekday, portfolio_total_asset_value=portfolio_total_asset_value
|
|
1179
|
-
)
|
|
1164
|
+
mock_compute_lookthrough.assert_called_once_with(lookthrough_portfolio, weekday)
|
|
@@ -142,11 +142,6 @@ class TestProductModel(PortfolioTestMixin):
|
|
|
142
142
|
|
|
143
143
|
def test_subquery_is_white_label_product(self, product, white_label_product):
|
|
144
144
|
product_queryset = Product.objects.all().annotate(is_white_label=Product.subquery_is_white_label_product())
|
|
145
|
-
# for product in product_queryset:
|
|
146
|
-
# print(product, product.is_white_label)
|
|
147
|
-
|
|
148
|
-
# print(product_queryset.filter(is_white_label=True).count())
|
|
149
|
-
|
|
150
145
|
assert product_queryset.filter(is_white_label=True).count() == 1
|
|
151
146
|
|
|
152
147
|
def test_annotate_aum(
|
|
@@ -310,12 +310,12 @@ class TestTradeProposal:
|
|
|
310
310
|
assert t3.weighting == Decimal("0.5")
|
|
311
311
|
|
|
312
312
|
# Test replaying trade proposals
|
|
313
|
-
@patch.object(Portfolio, "
|
|
313
|
+
@patch.object(Portfolio, "drift_weights")
|
|
314
314
|
def test_replay(self, mock_fct, trade_proposal_factory):
|
|
315
315
|
"""
|
|
316
|
-
Ensure replaying trade proposals correctly calls
|
|
316
|
+
Ensure replaying trade proposals correctly calls drift_weights for each period.
|
|
317
317
|
"""
|
|
318
|
-
mock_fct.return_value = None
|
|
318
|
+
mock_fct.return_value = None, None
|
|
319
319
|
|
|
320
320
|
# Create approved trade proposals for testing
|
|
321
321
|
tp0 = trade_proposal_factory.create(status=TradeProposal.Status.APPROVED)
|
|
@@ -333,14 +333,14 @@ class TestTradeProposal:
|
|
|
333
333
|
# Replay trade proposals
|
|
334
334
|
tp0.replay()
|
|
335
335
|
|
|
336
|
-
# Expected calls to
|
|
336
|
+
# Expected calls to drift_weights
|
|
337
337
|
expected_calls = [
|
|
338
338
|
call(tp0.trade_date, tp1.trade_date - timedelta(days=1)),
|
|
339
339
|
call(tp1.trade_date, tp2.trade_date - timedelta(days=1)),
|
|
340
340
|
call(tp2.trade_date, date.today()),
|
|
341
341
|
]
|
|
342
342
|
|
|
343
|
-
# Assert
|
|
343
|
+
# Assert drift_weights was called as expected
|
|
344
344
|
mock_fct.assert_has_calls(expected_calls)
|
|
345
345
|
|
|
346
346
|
# Test stopping replay on a non-approved proposal
|
|
@@ -396,3 +396,15 @@ class TestTradeProposal:
|
|
|
396
396
|
target_cash_position = trade_proposal.get_estimated_target_cash(trade_proposal.portfolio.currency)
|
|
397
397
|
assert target_cash_position.weighting == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
|
|
398
398
|
assert target_cash_position.initial_shares == Decimal(1_000_000) * Decimal("0.3")
|
|
399
|
+
|
|
400
|
+
def test_trade_proposal_update_inception_date(self, trade_proposal_factory, portfolio, instrument_factory):
|
|
401
|
+
# Check that if we create a prior trade proposal, the instrument inception date is updated accordingly
|
|
402
|
+
instrument = instrument_factory.create(inception_date=None)
|
|
403
|
+
instrument.portfolios.add(portfolio)
|
|
404
|
+
tp = trade_proposal_factory.create(portfolio=portfolio)
|
|
405
|
+
instrument.refresh_from_db()
|
|
406
|
+
assert instrument.inception_date == (tp.trade_date + BDay(1)).date()
|
|
407
|
+
|
|
408
|
+
tp2 = trade_proposal_factory.create(portfolio=portfolio, trade_date=tp.trade_date - BDay(1))
|
|
409
|
+
instrument.refresh_from_db()
|
|
410
|
+
assert instrument.inception_date == (tp2.trade_date + BDay(1)).date()
|
|
@@ -27,7 +27,7 @@ from wbportfolio.models import (
|
|
|
27
27
|
RebalancingModel,
|
|
28
28
|
TradeProposal,
|
|
29
29
|
)
|
|
30
|
-
from wbportfolio.models.portfolio import
|
|
30
|
+
from wbportfolio.models.portfolio import compute_lookthrough_as_task
|
|
31
31
|
from wbportfolio.serializers import (
|
|
32
32
|
PortfolioModelSerializer,
|
|
33
33
|
PortfolioPortfolioThroughModelSerializer,
|
|
@@ -173,7 +173,7 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
173
173
|
with suppress(KeyError):
|
|
174
174
|
start = datetime.strptime(request.POST["start"], "%Y-%m-%d")
|
|
175
175
|
end = datetime.strptime(request.POST["end"], "%Y-%m-%d")
|
|
176
|
-
|
|
176
|
+
compute_lookthrough_as_task.delay(portfolio.id, start, end)
|
|
177
177
|
return HttpResponse("Ok", status=200)
|
|
178
178
|
|
|
179
179
|
return HttpResponse("Bad arguments", status=400)
|
|
@@ -138,7 +138,7 @@ wbportfolio/import_export/parsers/natixis/d1_trade.py,sha256=jhUa35DGfN5pvGuGhnc
|
|
|
138
138
|
wbportfolio/import_export/parsers/natixis/d1_valuation.py,sha256=O6L-tCjfSApBTUTe2-azBJqyG3YfnhZvGjNhIFFx3A8,1290
|
|
139
139
|
wbportfolio/import_export/parsers/natixis/dividend.py,sha256=h_J7ZH724BkO_WtDo0Ycn9GfqtNEX9Wnjdf-iq52dOQ,2368
|
|
140
140
|
wbportfolio/import_export/parsers/natixis/equity.py,sha256=Aam3aI7rE3dTO4SrzctMnzCxQSYacRjkZsHrZyaBA-0,2474
|
|
141
|
-
wbportfolio/import_export/parsers/natixis/fees.py,sha256=
|
|
141
|
+
wbportfolio/import_export/parsers/natixis/fees.py,sha256=P3vicaOXAx_7zISlsyB3hezM6-skg4Hi8F59djYEsoY,1918
|
|
142
142
|
wbportfolio/import_export/parsers/natixis/trade.py,sha256=oIQGwL7k6T4H8d_XvhirvHsctCynsg_Ws1zbcJspfSE,2530
|
|
143
143
|
wbportfolio/import_export/parsers/natixis/utils.py,sha256=aCl14mKhm0PamlHI2gd9GQjpHKd-prH-iNRhsuY8yxQ,2910
|
|
144
144
|
wbportfolio/import_export/parsers/natixis/valuation.py,sha256=mLjIw1GBlPPlzHJkxg14kJddnKlOZjX8hL-bT227H_k,1538
|
|
@@ -148,7 +148,7 @@ wbportfolio/import_export/parsers/sg_lux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
|
|
|
148
148
|
wbportfolio/import_export/parsers/sg_lux/custodian_positions.py,sha256=hl-LqJbwojQOe0zGEH7W31i4hNTVme5L7nwXwyk6XBY,2483
|
|
149
149
|
wbportfolio/import_export/parsers/sg_lux/customer_trade.py,sha256=gTEUIaxlZvXXCgYyg9FD3D3aQhffhruYmeZlaAghrK8,2617
|
|
150
150
|
wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py,sha256=4KSxUGyflf3kY5BVJqMAF1aFkeSWEJegfDtLiA1BM6c,5065
|
|
151
|
-
wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py,sha256=
|
|
151
|
+
wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py,sha256=D_1c0v6lTXkopjZzr9pQb575HxrMFe91tKk8xT1X3Ns,3604
|
|
152
152
|
wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py,sha256=nMYV9-OwY_TagU36Ab0dOweZYzVriFWHXPYjauBg2Yk,2034
|
|
153
153
|
wbportfolio/import_export/parsers/sg_lux/equity.py,sha256=w13LAMNWReZzqmdFyXdazVnpGr1UUNLG-pJgn5c16gI,6116
|
|
154
154
|
wbportfolio/import_export/parsers/sg_lux/fees.py,sha256=xyPO9sYQyJW5w-1XhUMizY2RbH1Pr4n7LAUE-E67Yj4,1911
|
|
@@ -245,19 +245,19 @@ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4
|
|
|
245
245
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
246
246
|
wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
|
|
247
247
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
248
|
-
wbportfolio/models/asset.py,sha256
|
|
248
|
+
wbportfolio/models/asset.py,sha256=-bw8AoIIBy68je1vnGXYbkb7Vdp_mRBNJixd0qnCWxA,45153
|
|
249
249
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
250
250
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
251
251
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
252
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
253
|
-
wbportfolio/models/portfolio_cash_flow.py,sha256=
|
|
252
|
+
wbportfolio/models/portfolio.py,sha256=Rq9ih9aPhLC1RukrIF_wrmtRQm0-3w4i9NEtWJGZ02g,55742
|
|
253
|
+
wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
|
|
254
254
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
255
255
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
256
256
|
wbportfolio/models/portfolio_swing_pricings.py,sha256=_LqYC1VRjnnFFmVqFPRdnbgYsVxocMVpClTk2dnooig,1778
|
|
257
257
|
wbportfolio/models/product_groups.py,sha256=qoT8qv6tRdD6zmNDxsHO6dtnWw1-TRnyS5NqOq_gwhI,7901
|
|
258
258
|
wbportfolio/models/products.py,sha256=5z9OMLVS0ps6aOJbG3JE7gKTf3PlbC4rXIPpv55dD1A,22863
|
|
259
259
|
wbportfolio/models/registers.py,sha256=qA6T33t4gxFYnabQFBMd90WGIr6wxxirDLKDFqjOfok,4667
|
|
260
|
-
wbportfolio/models/roles.py,sha256=
|
|
260
|
+
wbportfolio/models/roles.py,sha256=wF5Atp0xsJxDrYjmFw9XHgEOe_4C-lm4pppeYdymOrA,7343
|
|
261
261
|
wbportfolio/models/utils.py,sha256=iBdMjRCvr6aOL0nLgfSCWUKe0h39h3IGmUbYo6l9t6w,394
|
|
262
262
|
wbportfolio/models/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
263
263
|
wbportfolio/models/graphs/portfolio.py,sha256=NwkehWvTcyTYrKO5ku3eNNaYLuBwuLdSbTEuugGuSIU,6541
|
|
@@ -276,8 +276,8 @@ wbportfolio/models/transactions/dividends.py,sha256=92-jG8bZN9nU9oDubpu-UDH43Ri7
|
|
|
276
276
|
wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
|
|
277
277
|
wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
|
|
278
278
|
wbportfolio/models/transactions/rebalancing.py,sha256=obzgewWKOD4kJbCoF5fhtfDk502QkbrjPKh8T9KDGew,7355
|
|
279
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
280
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
279
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=eWT_pTFLQ_4Eq8jVEfWhfewPVQ1Mx2viD0gAIosCNqI,30540
|
|
280
|
+
wbportfolio/models/transactions/trades.py,sha256=1kCgNXWeKvkRtoe8W7vjfzvpYIDwuU0jvgyCJY-IABc,28917
|
|
281
281
|
wbportfolio/models/transactions/transactions.py,sha256=fWoDf0TSV0L0gLUDOQpCRLzjMt1H4MUvUHGEaMsilCc,7027
|
|
282
282
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
283
283
|
wbportfolio/pms/typing.py,sha256=b2pBWYt1E8ok-Kqm0lEFIakSnWJ6Ib57z-VX3C3gkQc,6081
|
|
@@ -374,9 +374,9 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
374
374
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
375
375
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
376
376
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
377
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
377
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=2VqUF3HALY1VvCksARk4XJlQL-qr2PbF2DMmw9KQzdY,53132
|
|
378
378
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
379
|
-
wbportfolio/tests/models/test_products.py,sha256=
|
|
379
|
+
wbportfolio/tests/models/test_products.py,sha256=FkQLms3kXzyg6mNEEJcHUVKu8YbATY9l6lZyaRUnpjw,9314
|
|
380
380
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
381
381
|
wbportfolio/tests/models/test_splits.py,sha256=ytKcHsI_90kj1L4s8It-KEcc24rkDcElxwQ8q0QxEvk,9689
|
|
382
382
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
@@ -384,7 +384,7 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
384
384
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
385
385
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=1gp_h_CCC4Z_cWHUgrZCjGAxYuT2u8FZdw0krDpESiY,2801
|
|
386
386
|
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
|
|
387
|
-
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=
|
|
387
|
+
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=AhPI3XEjxzZxEUSzatCs6cOBnRHRig55NnV__PYubcQ,18675
|
|
388
388
|
wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
|
|
389
389
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
390
390
|
wbportfolio/tests/pms/test_analytics.py,sha256=fAuY1zcXibttFpBh2GhKVyzdYfi1kz_b7SPa9xZQXY0,1086
|
|
@@ -409,7 +409,7 @@ wbportfolio/viewsets/portfolio_cash_flow.py,sha256=jkBfdZRQ3KsxGMJpltRjmdrZ2qEFJ
|
|
|
409
409
|
wbportfolio/viewsets/portfolio_cash_targets.py,sha256=CvHlrDE8qnnnfRpTYnFu-Uu15MDbF5d5gTmEKth2S24,322
|
|
410
410
|
wbportfolio/viewsets/portfolio_relationship.py,sha256=RGyvxd8NfFEs8YdqEvVD3VbrISvAO5UtCTlocSIuWQw,2109
|
|
411
411
|
wbportfolio/viewsets/portfolio_swing_pricing.py,sha256=-57l3WLQZRslIV67OT0ucHE5JXTtTtLvd3t7MppdVn8,357
|
|
412
|
-
wbportfolio/viewsets/portfolios.py,sha256=
|
|
412
|
+
wbportfolio/viewsets/portfolios.py,sha256=FFK8Vt7Mk5ttS9FzGcRlKhTg0suX1wa8H35h4xZEe8Y,14438
|
|
413
413
|
wbportfolio/viewsets/positions.py,sha256=2rzFHB_SI09rXC_EYi58G_eqvzONbk8z61JDkkjt3Ew,13207
|
|
414
414
|
wbportfolio/viewsets/product_groups.py,sha256=YvmuXPPy98K1J_rz6YPsx9gNK-tCS2P-wc1uRYgfyo0,2399
|
|
415
415
|
wbportfolio/viewsets/product_performance.py,sha256=dRfRgifjGS1RgZSu9uJRM0SmB7eLnNUkPuqARMO4gyo,28371
|
|
@@ -521,7 +521,7 @@ wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQV
|
|
|
521
521
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=iQpC_Thbj56SmM05vPRsF1JZguGBDaTUH3I-_iCHCV0,5958
|
|
522
522
|
wbportfolio/viewsets/transactions/trades.py,sha256=-yJ4j8NJTu2VWyhCq5BXGNND_925Ietoxx9k07SLVh0,21634
|
|
523
523
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
524
|
-
wbportfolio-1.49.
|
|
525
|
-
wbportfolio-1.49.
|
|
526
|
-
wbportfolio-1.49.
|
|
527
|
-
wbportfolio-1.49.
|
|
524
|
+
wbportfolio-1.49.11.dist-info/METADATA,sha256=vbQQ5UCTEyqLH-YcOOF6BVCDXb3ujdegBOUX6k_iQ-8,735
|
|
525
|
+
wbportfolio-1.49.11.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
526
|
+
wbportfolio-1.49.11.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
527
|
+
wbportfolio-1.49.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|