wbportfolio 1.52.0__py2.py3-none-any.whl → 1.59.4__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbportfolio might be problematic. Click here for more details.
- wbportfolio/admin/__init__.py +3 -1
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +16 -0
- wbportfolio/admin/orders/orders.py +32 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -2
- wbportfolio/admin/transactions/dividends.py +40 -4
- wbportfolio/admin/transactions/fees.py +24 -14
- wbportfolio/admin/transactions/trades.py +34 -27
- wbportfolio/analysis/claims.py +5 -6
- wbportfolio/api_clients/ubs.py +162 -0
- wbportfolio/constants.py +1 -0
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/defaults/fees/default.py +7 -15
- wbportfolio/factories/__init__.py +2 -2
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/dividends.py +8 -3
- wbportfolio/factories/fees.py +8 -4
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +21 -0
- wbportfolio/factories/orders/orders.py +34 -0
- wbportfolio/factories/portfolios.py +2 -1
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +12 -16
- wbportfolio/filters/assets.py +18 -4
- wbportfolio/filters/orders/__init__.py +2 -0
- wbportfolio/filters/orders/order_proposals.py +55 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/filters/portfolios.py +38 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/__init__.py +1 -2
- wbportfolio/filters/transactions/fees.py +5 -12
- wbportfolio/filters/transactions/trades.py +16 -8
- wbportfolio/filters/transactions/utils.py +42 -0
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +22 -10
- wbportfolio/import_export/handlers/dividend.py +8 -8
- wbportfolio/import_export/handlers/fees.py +13 -23
- wbportfolio/import_export/handlers/orders.py +71 -0
- wbportfolio/import_export/handlers/trade.py +53 -77
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
- wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
- wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
- wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/fees.py +7 -9
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
- wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
- wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
- wbportfolio/import_export/resources/trades.py +3 -3
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
- wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
- wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
- wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
- wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
- wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +28 -170
- wbportfolio/models/builder.py +323 -0
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/orders/order_proposals.py +1414 -0
- wbportfolio/models/orders/orders.py +410 -0
- wbportfolio/models/portfolio.py +311 -289
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +12 -0
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/__init__.py +0 -4
- wbportfolio/models/transactions/claim.py +7 -6
- wbportfolio/models/transactions/dividends.py +42 -5
- wbportfolio/models/transactions/fees.py +55 -22
- wbportfolio/models/transactions/trades.py +121 -442
- wbportfolio/models/transactions/transactions.py +78 -158
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +35 -0
- wbportfolio/order_routing/adapters/__init__.py +65 -0
- wbportfolio/order_routing/adapters/ubs.py +195 -0
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/__init__.py +0 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/analytics/portfolio.py +17 -9
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +198 -63
- wbportfolio/rebalancing/base.py +12 -1
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +4 -8
- wbportfolio/rebalancing/models/equally_weighted.py +13 -11
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
- wbportfolio/rebalancing/models/model_portfolio.py +14 -18
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/orders/order_proposals.py +115 -0
- wbportfolio/serializers/orders/orders.py +283 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/signals.py +9 -12
- wbportfolio/serializers/transactions/__init__.py +1 -10
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/serializers/transactions/dividends.py +37 -9
- wbportfolio/serializers/transactions/fees.py +39 -10
- wbportfolio/serializers/transactions/trades.py +55 -157
- wbportfolio/tasks.py +43 -5
- wbportfolio/tests/analysis/__init__.py +0 -0
- wbportfolio/tests/analysis/test_claims.py +85 -0
- wbportfolio/tests/conftest.py +12 -12
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
- wbportfolio/tests/models/test_assets.py +7 -3
- wbportfolio/tests/models/test_imports.py +9 -13
- wbportfolio/tests/models/test_portfolios.py +102 -95
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_fees.py +7 -13
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/pms/test_analytics.py +22 -3
- wbportfolio/tests/rebalancing/test_models.py +51 -57
- wbportfolio/tests/signals.py +10 -20
- wbportfolio/tests/tests.py +3 -1
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +10 -13
- wbportfolio/viewsets/__init__.py +9 -4
- wbportfolio/viewsets/assets.py +3 -204
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +344 -154
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/__init__.py +2 -5
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/fees.py +3 -3
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/configs/display/trades.py +1 -189
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
- wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/configs/menu/__init__.py +1 -1
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/configs/titles/__init__.py +2 -3
- wbportfolio/viewsets/configs/titles/fees.py +4 -8
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/mixins.py +5 -1
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
- wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
- wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/orders/order_proposals.py +252 -0
- wbportfolio/viewsets/orders/orders.py +277 -0
- wbportfolio/viewsets/portfolios.py +36 -12
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/products.py +6 -6
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +3 -14
- wbportfolio/viewsets/transactions/fees.py +22 -22
- wbportfolio/viewsets/transactions/trades.py +1 -180
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/admin/transactions/transactions.py +0 -38
- wbportfolio/factories/transactions.py +0 -22
- wbportfolio/fdm/tasks.py +0 -13
- wbportfolio/filters/transactions/transactions.py +0 -99
- wbportfolio/models/transactions/expiry.py +0 -7
- wbportfolio/models/transactions/trade_proposals.py +0 -704
- wbportfolio/pms/trading/handler.py +0 -161
- wbportfolio/serializers/transactions/expiry.py +0 -18
- wbportfolio/serializers/transactions/trade_proposals.py +0 -76
- wbportfolio/serializers/transactions/transactions.py +0 -85
- wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
- wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
- wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
- wbportfolio/viewsets/configs/display/transactions.py +0 -55
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
- wbportfolio/viewsets/configs/menu/transactions.py +0 -9
- wbportfolio/viewsets/configs/titles/transactions.py +0 -9
- wbportfolio/viewsets/signals.py +0 -43
- wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
- wbportfolio/viewsets/transactions/transactions.py +0 -122
- /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -27,9 +27,13 @@ class TestAssetPositionModel:
|
|
|
27
27
|
asset_position_factory.create(portfolio=portfolio, underlying_instrument=equity)
|
|
28
28
|
assert AssetPosition.country_group_by(AssetPosition.objects.all()).values("groupby_id").distinct().count() == 1
|
|
29
29
|
|
|
30
|
-
def test_exchange_group_by(self, asset_position_factory, portfolio,
|
|
31
|
-
asset_position_factory.create(
|
|
32
|
-
|
|
30
|
+
def test_exchange_group_by(self, asset_position_factory, portfolio, exchange_factory, instrument):
|
|
31
|
+
asset_position_factory.create(
|
|
32
|
+
portfolio=portfolio, exchange=exchange_factory.create(), underlying_quote=instrument
|
|
33
|
+
)
|
|
34
|
+
asset_position_factory.create(
|
|
35
|
+
portfolio=portfolio, exchange=exchange_factory.create(), underlying_quote=instrument
|
|
36
|
+
)
|
|
33
37
|
assert (
|
|
34
38
|
AssetPosition.exchange_group_by(AssetPosition.objects.all()).values("groupby_id").distinct().count() == 1
|
|
35
39
|
)
|
|
@@ -27,21 +27,19 @@ class TestImportMixinModel:
|
|
|
27
27
|
data["currency"] = {"key": portfolio.currency.key}
|
|
28
28
|
del data["id"]
|
|
29
29
|
del data["import_source"]
|
|
30
|
-
del data["transaction_ptr"]
|
|
31
30
|
return data
|
|
32
31
|
|
|
33
32
|
trade = trade_factory.build()
|
|
34
33
|
data = {"data": [serialize(trade)]}
|
|
35
|
-
handler = TradeImportHandler(import_source)
|
|
36
34
|
|
|
37
35
|
# Import non existing data
|
|
38
|
-
|
|
36
|
+
TradeImportHandler(import_source).process(data)
|
|
39
37
|
assert Trade.objects.count() == 1
|
|
40
38
|
|
|
41
39
|
# Import already existing data
|
|
42
40
|
# import_source.data['data'][0]['shares'] *= 2
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
TradeImportHandler(import_source).process(data)
|
|
45
43
|
assert Trade.objects.count() == 1
|
|
46
44
|
|
|
47
45
|
def test_import_price(self, import_source, product, instrument_price_factory, instrument):
|
|
@@ -76,16 +74,12 @@ class TestImportMixinModel:
|
|
|
76
74
|
|
|
77
75
|
def serialize(fees):
|
|
78
76
|
data = model_to_dict(fees)
|
|
79
|
-
data["
|
|
80
|
-
data["
|
|
81
|
-
data["portfolio"] = portfolio.id
|
|
82
|
-
data["linked_product"] = product.id
|
|
83
|
-
data["portfolio"] = portfolio.id
|
|
77
|
+
data["fee_date"] = fees.fee_date.strftime("%Y-%m-%d")
|
|
78
|
+
data["product"] = product.id
|
|
84
79
|
data["currency"] = {"key": portfolio.currency.key}
|
|
85
80
|
del data["calculated"]
|
|
86
81
|
del data["id"]
|
|
87
82
|
del data["import_source"]
|
|
88
|
-
del data["transaction_ptr"]
|
|
89
83
|
return data
|
|
90
84
|
|
|
91
85
|
fees = fees_factory.build(calculated=False)
|
|
@@ -142,7 +136,9 @@ class TestImportMixinModel:
|
|
|
142
136
|
data = {
|
|
143
137
|
"data": [
|
|
144
138
|
self._serialize_position(
|
|
145
|
-
asset_position_factory.build(
|
|
139
|
+
asset_position_factory.build(
|
|
140
|
+
date=val_date, underlying_instrument=instrument, weighting=Decimal("0.25")
|
|
141
|
+
),
|
|
146
142
|
product_portfolio,
|
|
147
143
|
instrument,
|
|
148
144
|
)
|
|
@@ -176,7 +172,7 @@ class TestImportMixinModel:
|
|
|
176
172
|
def test_import_assetposition_product_group(
|
|
177
173
|
self, import_source, product_group, currency, equity, asset_position_factory
|
|
178
174
|
):
|
|
179
|
-
positions = asset_position_factory.build(underlying_instrument=equity)
|
|
175
|
+
positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
|
|
180
176
|
data = {"data": [self._serialize_position(positions, product_group, equity)]}
|
|
181
177
|
|
|
182
178
|
# Import non existing data
|
|
@@ -188,7 +184,7 @@ class TestImportMixinModel:
|
|
|
188
184
|
def test_import_assetposition_index(
|
|
189
185
|
self, import_source, index, portfolio, currency, equity, asset_position_factory
|
|
190
186
|
):
|
|
191
|
-
positions = asset_position_factory.build(underlying_instrument=equity)
|
|
187
|
+
positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
|
|
192
188
|
index.portfolios.add(portfolio)
|
|
193
189
|
data = {"data": [self._serialize_position(positions, index, equity)]}
|
|
194
190
|
|
|
@@ -5,12 +5,12 @@ from unittest.mock import patch
|
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
import pytest
|
|
8
|
-
from django.contrib.contenttypes.models import ContentType
|
|
9
8
|
from django.db.models import F, Sum
|
|
10
9
|
from django.forms.models import model_to_dict
|
|
11
10
|
from faker import Faker
|
|
12
11
|
from pandas.tseries.offsets import BDay
|
|
13
12
|
from psycopg.types.range import DateRange
|
|
13
|
+
from wbcore.contrib.currency.factories import CurrencyFactory
|
|
14
14
|
from wbcore.contrib.geography.factories import CountryFactory
|
|
15
15
|
|
|
16
16
|
from wbportfolio.models import (
|
|
@@ -20,9 +20,8 @@ from wbportfolio.models import (
|
|
|
20
20
|
PortfolioPortfolioThroughModel,
|
|
21
21
|
Trade,
|
|
22
22
|
)
|
|
23
|
-
from wbportfolio.models.asset import AssetPositionIterator
|
|
24
23
|
|
|
25
|
-
from ...models.portfolio import
|
|
24
|
+
from ...models.portfolio import update_portfolio_after_investable_universe
|
|
26
25
|
from .utils import PortfolioTestMixin
|
|
27
26
|
|
|
28
27
|
fake = Faker()
|
|
@@ -108,11 +107,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
108
107
|
)
|
|
109
108
|
assert portfolio.get_geographical_breakdown(weekday).shape[0] == 2
|
|
110
109
|
|
|
111
|
-
def test_get_currency_exposure(self, portfolio, asset_position_factory,
|
|
110
|
+
def test_get_currency_exposure(self, portfolio, asset_position_factory, equity_factory, weekday):
|
|
112
111
|
a1 = asset_position_factory.create(
|
|
113
112
|
portfolio=portfolio,
|
|
114
|
-
underlying_instrument=equity_factory.create(),
|
|
115
|
-
currency=currency_factory.create(),
|
|
113
|
+
underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
|
|
116
114
|
date=weekday,
|
|
117
115
|
)
|
|
118
116
|
asset_position_factory.create(
|
|
@@ -120,8 +118,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
120
118
|
)
|
|
121
119
|
asset_position_factory.create(
|
|
122
120
|
portfolio=portfolio,
|
|
123
|
-
underlying_instrument=equity_factory.create(),
|
|
124
|
-
currency=currency_factory.create(),
|
|
121
|
+
underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
|
|
125
122
|
date=weekday,
|
|
126
123
|
)
|
|
127
124
|
assert portfolio.get_currency_exposure(weekday).shape[0] == 2
|
|
@@ -255,16 +252,18 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
255
252
|
|
|
256
253
|
@patch.object(Portfolio, "estimate_net_asset_values", autospec=True)
|
|
257
254
|
def test_change_at_date(self, mock_estimate_net_asset_values, asset_position_factory, portfolio, weekday):
|
|
258
|
-
asset_position_factory.
|
|
255
|
+
a1 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.1"))
|
|
256
|
+
a2 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.3"))
|
|
257
|
+
a3 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.5"))
|
|
259
258
|
|
|
260
|
-
portfolio.change_at_date(weekday,
|
|
259
|
+
portfolio.change_at_date(weekday, fix_quantization=True)
|
|
261
260
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for pos in AssetPosition.objects.all():
|
|
265
|
-
assert float(pos.weighting) == pytest.approx(float(pos.total_value_fx_portfolio / total_value), rel=1e-2)
|
|
261
|
+
cash_pos = portfolio.assets.get(date=weekday, portfolio=portfolio, underlying_quote=portfolio.cash_component)
|
|
262
|
+
assert cash_pos.weighting == Decimal("1.0") - (a1.weighting + a2.weighting + a3.weighting)
|
|
266
263
|
|
|
267
|
-
mock_estimate_net_asset_values.assert_called_once_with(
|
|
264
|
+
mock_estimate_net_asset_values.assert_called_once_with(
|
|
265
|
+
portfolio, (weekday + BDay(1)).date(), analytic_portfolio=None
|
|
266
|
+
)
|
|
268
267
|
|
|
269
268
|
@patch.object(Portfolio, "compute_lookthrough", autospec=True)
|
|
270
269
|
def test_change_at_date_with_dependent_portfolio(
|
|
@@ -282,7 +281,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
282
281
|
dependent_portfolio.depends_on.add(base_portfolio)
|
|
283
282
|
base_portfolio.change_at_date(weekday)
|
|
284
283
|
|
|
285
|
-
mock_compute_lookthrough.assert_called_once_with(
|
|
284
|
+
mock_compute_lookthrough.assert_called_once_with(
|
|
285
|
+
dependent_portfolio,
|
|
286
|
+
weekday,
|
|
287
|
+
)
|
|
286
288
|
|
|
287
289
|
def test_is_active_at_date(
|
|
288
290
|
self,
|
|
@@ -527,8 +529,9 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
527
529
|
assert res1
|
|
528
530
|
|
|
529
531
|
def test_get_total_asset_under_management(
|
|
530
|
-
self,
|
|
532
|
+
self, portfolio_factory, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
|
|
531
533
|
):
|
|
534
|
+
portfolio = portfolio_factory.create()
|
|
532
535
|
i1 = instrument_factory.create()
|
|
533
536
|
i2 = instrument_factory.create()
|
|
534
537
|
previous_day = (weekday - BDay(5)).date()
|
|
@@ -921,13 +924,13 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
921
924
|
|
|
922
925
|
analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
|
|
923
926
|
assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
|
|
924
|
-
|
|
927
|
+
expected_x = pd.DataFrame(
|
|
925
928
|
[[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
|
|
926
929
|
columns=[i1.id, i2.id],
|
|
927
930
|
index=[(weekday + BDay(1)).date()],
|
|
928
931
|
)
|
|
929
|
-
|
|
930
|
-
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)
|
|
931
934
|
|
|
932
935
|
def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
|
|
933
936
|
a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
|
|
@@ -998,20 +1001,18 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
998
1001
|
fx_portfolio = currency_fx_rates_factory.create(currency=portfolio.currency, date=weekday)
|
|
999
1002
|
fx_instrument = currency_fx_rates_factory.create(currency=instrument.currency, date=weekday)
|
|
1000
1003
|
instrument_id: int = instrument.id
|
|
1001
|
-
weights = {instrument_id: random.random()}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
)
|
|
1005
|
-
positions.add((weekday, weights))
|
|
1004
|
+
weights = {instrument_id: Decimal(random.random())}
|
|
1005
|
+
portfolio.builder.prices = {weekday: {instrument_id: p.net_value}}
|
|
1006
|
+
portfolio.builder.add((weekday, weights), infer_underlying_quote_price=False)
|
|
1006
1007
|
|
|
1007
|
-
res = list(
|
|
1008
|
+
res = list(portfolio.builder.get_positions())
|
|
1008
1009
|
a = res[0]
|
|
1009
1010
|
assert len(res) == 1
|
|
1010
1011
|
assert a.date == weekday
|
|
1011
1012
|
assert a.underlying_quote == instrument
|
|
1012
|
-
assert a.underlying_quote_price
|
|
1013
|
+
assert a.underlying_quote_price is None
|
|
1013
1014
|
assert a.initial_price == p.net_value
|
|
1014
|
-
assert a.weighting == weights[instrument.id]
|
|
1015
|
+
assert a.weighting == pytest.approx(weights[instrument.id], abs=Decimal(10e-6))
|
|
1015
1016
|
assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
|
|
1016
1017
|
assert a.currency_fx_rate_instrument_to_usd == fx_instrument
|
|
1017
1018
|
|
|
@@ -1033,45 +1034,48 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1033
1034
|
|
|
1034
1035
|
i1 = instrument_factory.create(currency=portfolio.currency)
|
|
1035
1036
|
i2 = instrument_factory.create(currency=portfolio.currency)
|
|
1037
|
+
|
|
1038
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=(weekday - BDay(1)).date())
|
|
1039
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=weekday)
|
|
1040
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=middle_date)
|
|
1041
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=rebalancing_date)
|
|
1042
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=(weekday - BDay(1)).date())
|
|
1043
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=weekday)
|
|
1044
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=middle_date)
|
|
1045
|
+
instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=rebalancing_date)
|
|
1046
|
+
|
|
1036
1047
|
asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i1, weighting=0.7)
|
|
1037
1048
|
asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
|
|
1038
1049
|
|
|
1039
|
-
instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date())
|
|
1040
|
-
instrument_price_factory.create(instrument=i1, date=weekday)
|
|
1041
|
-
instrument_price_factory.create(instrument=i1, date=middle_date)
|
|
1042
|
-
instrument_price_factory.create(instrument=i1, date=rebalancing_date)
|
|
1043
|
-
instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date())
|
|
1044
|
-
instrument_price_factory.create(instrument=i2, date=weekday)
|
|
1045
|
-
instrument_price_factory.create(instrument=i2, date=middle_date)
|
|
1046
|
-
instrument_price_factory.create(instrument=i2, date=rebalancing_date)
|
|
1047
|
-
|
|
1048
1050
|
rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
assert
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1051
|
+
portfolio.load_builder_returns(weekday, rebalancing_date)
|
|
1052
|
+
gen = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date(), stop_at_rebalancing=True)
|
|
1053
|
+
assert next(gen)[0] == middle_date, "Drifting weight with a non automatic rebalancer stops the iteration"
|
|
1054
|
+
try:
|
|
1055
|
+
next(gen)
|
|
1056
|
+
raise AssertionError("the next iteration should stop and return the rebalancing")
|
|
1057
|
+
except StopIteration as e:
|
|
1058
|
+
rebalancing_order_proposal = e.value
|
|
1059
|
+
assert rebalancing_order_proposal.trade_date == rebalancing_date
|
|
1060
|
+
assert rebalancing_order_proposal.status == "PENDING"
|
|
1061
|
+
|
|
1062
|
+
# we expect a equally rebalancing (default) so both orders needs to be created
|
|
1063
|
+
orders = rebalancing_order_proposal.get_orders()
|
|
1064
|
+
t1 = orders.get(value_date=rebalancing_date, underlying_instrument=i1)
|
|
1065
|
+
t2 = orders.get(value_date=rebalancing_date, underlying_instrument=i2)
|
|
1066
|
+
assert t1._target_weight == Decimal("0.5")
|
|
1067
|
+
assert t2._target_weight == Decimal("0.5")
|
|
1068
|
+
|
|
1069
|
+
rebalancing_order_proposal.approve()
|
|
1070
|
+
rebalancing_order_proposal.save()
|
|
1071
|
+
# we approve the rebalancing order proposal
|
|
1072
|
+
assert rebalancing_order_proposal.status == "APPROVED"
|
|
1066
1073
|
|
|
1067
1074
|
# check that the rebalancing was applied and position reflect that
|
|
1068
1075
|
assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i1).weighting == Decimal("0.5")
|
|
1069
1076
|
assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i2).weighting == Decimal("0.5")
|
|
1070
1077
|
|
|
1071
|
-
|
|
1072
|
-
def test_bulk_create_positions(
|
|
1073
|
-
self, mock_compute_metrics, portfolio, weekday, asset_position_factory, instrument_factory
|
|
1074
|
-
):
|
|
1078
|
+
def test_bulk_create_positions(self, portfolio, weekday, asset_position_factory, instrument_factory):
|
|
1075
1079
|
portfolio.is_manageable = False
|
|
1076
1080
|
portfolio.save()
|
|
1077
1081
|
i1 = instrument_factory.create()
|
|
@@ -1080,21 +1084,17 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1080
1084
|
a1 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i1)
|
|
1081
1085
|
|
|
1082
1086
|
# check initial creation
|
|
1083
|
-
portfolio.
|
|
1087
|
+
portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False, compute_metrics=True)
|
|
1084
1088
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a1.weighting
|
|
1085
1089
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i1
|
|
1086
1090
|
|
|
1087
|
-
|
|
1088
|
-
weekday, basket_id=portfolio.id, basket_content_type_id=ContentType.objects.get_for_model(Portfolio).id
|
|
1089
|
-
)
|
|
1090
|
-
|
|
1091
|
-
# check that if we change key value, an already exising position will be updated accordingly
|
|
1091
|
+
# check that if we change key value, an already existing position will be updated accordingly
|
|
1092
1092
|
a1.weighting = Decimal(0.5)
|
|
1093
|
-
portfolio.
|
|
1093
|
+
portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False)
|
|
1094
1094
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == Decimal(0.5)
|
|
1095
1095
|
|
|
1096
1096
|
a2 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i2)
|
|
1097
|
-
portfolio.
|
|
1097
|
+
portfolio.builder.add([a2]).bulk_create_positions(fix_quantization=False)
|
|
1098
1098
|
assert (
|
|
1099
1099
|
AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1).weighting
|
|
1100
1100
|
== a1.weighting
|
|
@@ -1105,7 +1105,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1105
1105
|
)
|
|
1106
1106
|
|
|
1107
1107
|
a3 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i3)
|
|
1108
|
-
portfolio.
|
|
1108
|
+
portfolio.builder.add([a3]).bulk_create_positions(delete_leftovers=True, fix_quantization=False)
|
|
1109
1109
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a3.weighting
|
|
1110
1110
|
assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i3
|
|
1111
1111
|
|
|
@@ -1124,33 +1124,6 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1124
1124
|
res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
|
|
1125
1125
|
assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
|
|
1126
1126
|
|
|
1127
|
-
def test_get_returns(self, instrument_factory, instrument_price_factory, asset_position_factory, portfolio):
|
|
1128
|
-
v1 = date(2025, 1, 1)
|
|
1129
|
-
v2 = date(2025, 1, 2)
|
|
1130
|
-
v3 = date(2025, 1, 3)
|
|
1131
|
-
|
|
1132
|
-
i1 = instrument_factory.create()
|
|
1133
|
-
i2 = instrument_factory.create()
|
|
1134
|
-
|
|
1135
|
-
i11 = instrument_price_factory.create(date=v1, instrument=i1)
|
|
1136
|
-
i12 = instrument_price_factory.create(date=v2, instrument=i1)
|
|
1137
|
-
i13 = instrument_price_factory.create(date=v3, instrument=i1)
|
|
1138
|
-
asset_position_factory.create(date=v1, portfolio=portfolio, underlying_instrument=i1)
|
|
1139
|
-
asset_position_factory.create(date=v3, portfolio=portfolio, underlying_instrument=i2)
|
|
1140
|
-
i11.refresh_from_db()
|
|
1141
|
-
i12.refresh_from_db()
|
|
1142
|
-
i13.refresh_from_db()
|
|
1143
|
-
returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
|
|
1144
|
-
|
|
1145
|
-
expected_returns = pd.DataFrame(
|
|
1146
|
-
[[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
|
|
1147
|
-
index=[v2, v3],
|
|
1148
|
-
columns=[i1.id, i2.id],
|
|
1149
|
-
dtype="float64",
|
|
1150
|
-
)
|
|
1151
|
-
expected_returns.index = pd.to_datetime(expected_returns.index)
|
|
1152
|
-
pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
|
|
1153
|
-
|
|
1154
1127
|
@patch.object(Portfolio, "compute_lookthrough", autospec=True)
|
|
1155
1128
|
def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
|
|
1156
1129
|
primary_portfolio = portfolio_factory.create(only_weighting=True)
|
|
@@ -1158,8 +1131,42 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1158
1131
|
PortfolioPortfolioThroughModel.objects.create(
|
|
1159
1132
|
portfolio=lookthrough_portfolio,
|
|
1160
1133
|
dependency_portfolio=primary_portfolio,
|
|
1161
|
-
type=PortfolioPortfolioThroughModel.Type.
|
|
1134
|
+
type=PortfolioPortfolioThroughModel.Type.LOOK_THROUGH,
|
|
1162
1135
|
)
|
|
1163
1136
|
|
|
1164
1137
|
primary_portfolio.handle_controlling_portfolio_change_at_date(weekday)
|
|
1165
1138
|
mock_compute_lookthrough.assert_called_once_with(lookthrough_portfolio, weekday)
|
|
1139
|
+
|
|
1140
|
+
def test_get_model_portfolio_relationships(self, portfolio_factory, asset_position_factory, weekday):
|
|
1141
|
+
model_portfolio = portfolio_factory.create()
|
|
1142
|
+
model_index = model_portfolio.get_or_create_index()
|
|
1143
|
+
dependent_portfolio = portfolio_factory.create()
|
|
1144
|
+
re1 = PortfolioPortfolioThroughModel.objects.create(
|
|
1145
|
+
portfolio=dependent_portfolio,
|
|
1146
|
+
dependency_portfolio=model_portfolio,
|
|
1147
|
+
type=PortfolioPortfolioThroughModel.Type.MODEL,
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
|
|
1151
|
+
re1,
|
|
1152
|
+
}
|
|
1153
|
+
parent_portfolio = portfolio_factory.create()
|
|
1154
|
+
child_portfolio = portfolio_factory.create()
|
|
1155
|
+
re2 = PortfolioPortfolioThroughModel.objects.create(
|
|
1156
|
+
portfolio=child_portfolio,
|
|
1157
|
+
dependency_portfolio=parent_portfolio,
|
|
1158
|
+
type=PortfolioPortfolioThroughModel.Type.MODEL,
|
|
1159
|
+
)
|
|
1160
|
+
assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
|
|
1161
|
+
re1,
|
|
1162
|
+
} # child portfolio is not considered in the tree because there is no position yet
|
|
1163
|
+
|
|
1164
|
+
asset_position_factory.create(portfolio=parent_portfolio, underlying_instrument=model_index, date=weekday)
|
|
1165
|
+
assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {re1, re2}
|
|
1166
|
+
|
|
1167
|
+
dependent_portfolio.is_active = False # disable this portfolio
|
|
1168
|
+
dependent_portfolio.deletion_datetime = weekday - timedelta(days=1)
|
|
1169
|
+
dependent_portfolio.save()
|
|
1170
|
+
assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
|
|
1171
|
+
re2,
|
|
1172
|
+
}
|
|
@@ -215,3 +215,14 @@ class TestProductModel(PortfolioTestMixin):
|
|
|
215
215
|
body=f"The product {product} will be terminated on the {product.delisted_date:%Y-%m-%d}",
|
|
216
216
|
user=internal_user,
|
|
217
217
|
)
|
|
218
|
+
|
|
219
|
+
def test_delist_product_disable_report(self, product, report_factory):
|
|
220
|
+
report = report_factory.create(content_object=product, is_active=True)
|
|
221
|
+
assert product.delisted_date is None
|
|
222
|
+
assert report.is_active
|
|
223
|
+
|
|
224
|
+
product.delisted_date = datetime.date.today()
|
|
225
|
+
product.save()
|
|
226
|
+
|
|
227
|
+
report.refresh_from_db()
|
|
228
|
+
assert report.is_active is False
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -2,7 +2,6 @@ from datetime import timedelta
|
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from wbfdm.factories import CashFactory
|
|
6
5
|
from wbfdm.models import InstrumentPrice
|
|
7
6
|
|
|
8
7
|
from wbportfolio.models import FeeCalculation, Fees, Product
|
|
@@ -11,19 +10,14 @@ from wbportfolio.models import FeeCalculation, Fees, Product
|
|
|
11
10
|
def fees_calculation(price_id):
|
|
12
11
|
price = InstrumentPrice.objects.get(id=price_id)
|
|
13
12
|
product = Product.objects.get(id=price.instrument.id)
|
|
14
|
-
cash = CashFactory.create(currency=product.currency)
|
|
15
13
|
yield {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"transaction_date": price.date,
|
|
14
|
+
"product": product,
|
|
15
|
+
"fee_date": price.date,
|
|
19
16
|
"transaction_subtype": Fees.Type.MANAGEMENT,
|
|
20
|
-
"underlying_instrument": cash,
|
|
21
17
|
"currency": product.currency,
|
|
22
18
|
"calculated": True,
|
|
23
19
|
"total_value": price.net_value,
|
|
24
|
-
"total_value_fx_portfolio": price.net_value,
|
|
25
20
|
"total_value_gross": price.net_value,
|
|
26
|
-
"total_value_gross_fx_portfolio": price.net_value,
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
|
|
@@ -38,14 +32,14 @@ class TestFeesModel:
|
|
|
38
32
|
) # no matter if estimated or not, we expect this fee to be on the resulting queryset
|
|
39
33
|
calculated_fees_d1 = fees_factory.create( # there will be a real fee for that date, type and product, so this will be filtered out
|
|
40
34
|
calculated=True,
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
product=fees_d0.product,
|
|
36
|
+
fee_date=(fees_d0.fee_date + timedelta(days=1)),
|
|
43
37
|
transaction_subtype=fees_d0.transaction_subtype,
|
|
44
38
|
)
|
|
45
39
|
real_fees_d1 = fees_factory.create(
|
|
46
40
|
calculated=False,
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
product=calculated_fees_d1.product,
|
|
42
|
+
fee_date=calculated_fees_d1.fee_date,
|
|
49
43
|
transaction_subtype=calculated_fees_d1.transaction_subtype,
|
|
50
44
|
)
|
|
51
45
|
|
|
@@ -61,6 +55,6 @@ class TestFeesModel:
|
|
|
61
55
|
price = instrument_price_factory.create(instrument=product) # post save must be called to compute fees
|
|
62
56
|
|
|
63
57
|
fees = Fees.objects.get(
|
|
64
|
-
|
|
58
|
+
product=product, fee_date=price.date, calculated=True, transaction_subtype=Fees.Type.MANAGEMENT
|
|
65
59
|
)
|
|
66
60
|
assert fees.total_value == pytest.approx(price.net_value, rel=Decimal(1e-4))
|