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
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
# Import necessary modules
|
|
2
|
-
from datetime import date, timedelta
|
|
3
|
-
from decimal import Decimal
|
|
4
|
-
from unittest.mock import call, patch
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
from faker import Faker
|
|
8
|
-
from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
|
|
9
|
-
|
|
10
|
-
from wbportfolio.models import Portfolio, RebalancingModel, TradeProposal
|
|
11
|
-
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
12
|
-
from wbportfolio.pms.typing import Position
|
|
13
|
-
|
|
14
|
-
fake = Faker()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Mark tests to use Django's database
|
|
18
|
-
@pytest.mark.django_db
|
|
19
|
-
class TestTradeProposal:
|
|
20
|
-
# Test that the checked object is correctly set to the portfolio
|
|
21
|
-
def test_checked_object(self, trade_proposal):
|
|
22
|
-
"""
|
|
23
|
-
Verify that the checked object is the portfolio associated with the trade proposal.
|
|
24
|
-
"""
|
|
25
|
-
assert trade_proposal.checked_object == trade_proposal.portfolio
|
|
26
|
-
|
|
27
|
-
# Test that the evaluation date matches the trade date
|
|
28
|
-
def test_check_evaluation_date(self, trade_proposal):
|
|
29
|
-
"""
|
|
30
|
-
Ensure the evaluation date is the same as the trade date.
|
|
31
|
-
"""
|
|
32
|
-
assert trade_proposal.check_evaluation_date == trade_proposal.trade_date
|
|
33
|
-
|
|
34
|
-
# Test the validated trading service functionality
|
|
35
|
-
def test_validated_trading_service(self, trade_proposal, asset_position_factory, trade_factory):
|
|
36
|
-
"""
|
|
37
|
-
Validate that the effective and target portfolios are correctly calculated.
|
|
38
|
-
"""
|
|
39
|
-
effective_date = (trade_proposal.trade_date - BDay(1)).date()
|
|
40
|
-
|
|
41
|
-
# Create asset positions for testing
|
|
42
|
-
a1 = asset_position_factory.create(
|
|
43
|
-
portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
|
|
44
|
-
)
|
|
45
|
-
a2 = asset_position_factory.create(
|
|
46
|
-
portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# Create trades for testing
|
|
50
|
-
t1 = trade_factory.create(
|
|
51
|
-
trade_proposal=trade_proposal,
|
|
52
|
-
weighting=Decimal("0.05"),
|
|
53
|
-
portfolio=trade_proposal.portfolio,
|
|
54
|
-
transaction_date=trade_proposal.trade_date,
|
|
55
|
-
underlying_instrument=a1.underlying_quote,
|
|
56
|
-
)
|
|
57
|
-
t2 = trade_factory.create(
|
|
58
|
-
trade_proposal=trade_proposal,
|
|
59
|
-
weighting=Decimal("-0.05"),
|
|
60
|
-
portfolio=trade_proposal.portfolio,
|
|
61
|
-
transaction_date=trade_proposal.trade_date,
|
|
62
|
-
underlying_instrument=a2.underlying_quote,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Get the validated trading service
|
|
66
|
-
validated_trading_service = trade_proposal.validated_trading_service
|
|
67
|
-
|
|
68
|
-
# Assert effective and target portfolios are as expected
|
|
69
|
-
assert validated_trading_service.effective_portfolio.to_dict() == {
|
|
70
|
-
a1.underlying_quote.id: a1.weighting,
|
|
71
|
-
a2.underlying_quote.id: a2.weighting,
|
|
72
|
-
}
|
|
73
|
-
assert validated_trading_service.target_portfolio.to_dict() == {
|
|
74
|
-
a1.underlying_quote.id: a1.weighting + t1.weighting,
|
|
75
|
-
a2.underlying_quote.id: a2.weighting + t2.weighting,
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
# Test the calculation of the last effective date
|
|
79
|
-
def test_last_effective_date(self, trade_proposal, asset_position_factory):
|
|
80
|
-
"""
|
|
81
|
-
Verify the last effective date is correctly determined based on asset positions.
|
|
82
|
-
"""
|
|
83
|
-
# Without any positions, it should be the day before the trade date
|
|
84
|
-
assert (
|
|
85
|
-
trade_proposal.last_effective_date == (trade_proposal.trade_date - BDay(1)).date()
|
|
86
|
-
), "Last effective date without position should be t-1"
|
|
87
|
-
|
|
88
|
-
# Create an asset position before the trade date
|
|
89
|
-
a1 = asset_position_factory.create(
|
|
90
|
-
portfolio=trade_proposal.portfolio, date=(trade_proposal.trade_date - BDay(5)).date()
|
|
91
|
-
)
|
|
92
|
-
a_noise = asset_position_factory.create(portfolio=trade_proposal.portfolio, date=trade_proposal.trade_date) # noqa
|
|
93
|
-
|
|
94
|
-
# The last effective date should still be the day before the trade date due to caching
|
|
95
|
-
assert (
|
|
96
|
-
trade_proposal.last_effective_date == (trade_proposal.trade_date - BDay(1)).date()
|
|
97
|
-
), "last effective date is cached, so it won't change as is"
|
|
98
|
-
|
|
99
|
-
# Reset the cache property to recalculate
|
|
100
|
-
del trade_proposal.last_effective_date
|
|
101
|
-
|
|
102
|
-
# Now it should be the date of the latest position before the trade date
|
|
103
|
-
assert (
|
|
104
|
-
trade_proposal.last_effective_date == a1.date
|
|
105
|
-
), "last effective date is the latest position strictly lower than trade date"
|
|
106
|
-
|
|
107
|
-
# Test finding the previous trade proposal
|
|
108
|
-
def test_previous_trade_proposal(self, trade_proposal_factory):
|
|
109
|
-
"""
|
|
110
|
-
Ensure the previous trade proposal is correctly identified as the last approved proposal before the current one.
|
|
111
|
-
"""
|
|
112
|
-
tp = trade_proposal_factory.create()
|
|
113
|
-
tp_previous_submit = trade_proposal_factory.create( # noqa
|
|
114
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date - BDay(1)).date()
|
|
115
|
-
)
|
|
116
|
-
tp_previous_approve = trade_proposal_factory.create(
|
|
117
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(2)).date()
|
|
118
|
-
)
|
|
119
|
-
tp_next_approve = trade_proposal_factory.create( # noqa
|
|
120
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(1)).date()
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
# The previous valid trade proposal should be the approved one strictly before the current proposal
|
|
124
|
-
assert (
|
|
125
|
-
tp.previous_trade_proposal == tp_previous_approve
|
|
126
|
-
), "the previous valid trade proposal is the strictly before and approved trade proposal"
|
|
127
|
-
|
|
128
|
-
# Test finding the next trade proposal
|
|
129
|
-
def test_next_trade_proposal(self, trade_proposal_factory):
|
|
130
|
-
"""
|
|
131
|
-
Verify the next trade proposal is correctly identified as the first approved proposal after the current one.
|
|
132
|
-
"""
|
|
133
|
-
tp = trade_proposal_factory.create()
|
|
134
|
-
tp_next_submit = trade_proposal_factory.create( # noqa
|
|
135
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
|
|
136
|
-
)
|
|
137
|
-
tp_next_approve = trade_proposal_factory.create(
|
|
138
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
|
|
139
|
-
)
|
|
140
|
-
tp_previous_approve = trade_proposal_factory.create( # noqa
|
|
141
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
# The next valid trade proposal should be the approved one strictly after the current proposal
|
|
145
|
-
assert (
|
|
146
|
-
tp.next_trade_proposal == tp_next_approve
|
|
147
|
-
), "the next valid trade proposal is the strictly after and approved trade proposal"
|
|
148
|
-
|
|
149
|
-
# Test getting the default target portfolio
|
|
150
|
-
def test__get_default_target_portfolio(self, trade_proposal, asset_position_factory):
|
|
151
|
-
"""
|
|
152
|
-
Ensure the default target portfolio is set to the effective portfolio from the day before the trade date.
|
|
153
|
-
"""
|
|
154
|
-
effective_date = (trade_proposal.trade_date - BDay(1)).date()
|
|
155
|
-
|
|
156
|
-
# Create asset positions for testing
|
|
157
|
-
a1 = asset_position_factory.create(
|
|
158
|
-
portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
|
|
159
|
-
)
|
|
160
|
-
a2 = asset_position_factory.create(
|
|
161
|
-
portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
|
|
162
|
-
)
|
|
163
|
-
asset_position_factory.create(portfolio=trade_proposal.portfolio, date=trade_proposal.trade_date) # noise
|
|
164
|
-
|
|
165
|
-
# The default target portfolio should match the effective portfolio
|
|
166
|
-
assert trade_proposal._get_default_target_portfolio().to_dict() == {
|
|
167
|
-
a1.underlying_quote.id: a1.weighting,
|
|
168
|
-
a2.underlying_quote.id: a2.weighting,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
# Test getting the default target portfolio with a rebalancing model
|
|
172
|
-
@patch.object(RebalancingModel, "get_target_portfolio")
|
|
173
|
-
def test__get_default_target_portfolio_with_rebalancer_model(self, mock_fct, trade_proposal, rebalancer_factory):
|
|
174
|
-
"""
|
|
175
|
-
Verify that the target portfolio is correctly obtained from a rebalancing model.
|
|
176
|
-
"""
|
|
177
|
-
# Expected target portfolio from the rebalancing model
|
|
178
|
-
expected_target_portfolio = PortfolioDTO(
|
|
179
|
-
positions=(Position(underlying_instrument=1, weighting=Decimal(1), date=trade_proposal.trade_date),)
|
|
180
|
-
)
|
|
181
|
-
mock_fct.return_value = expected_target_portfolio
|
|
182
|
-
|
|
183
|
-
# Create a rebalancer for testing
|
|
184
|
-
rebalancer = rebalancer_factory.create(
|
|
185
|
-
portfolio=trade_proposal.portfolio, parameters={"rebalancer_parameter": "A"}
|
|
186
|
-
)
|
|
187
|
-
trade_proposal.rebalancing_model = rebalancer.rebalancing_model
|
|
188
|
-
trade_proposal.save()
|
|
189
|
-
|
|
190
|
-
# Additional keyword arguments for the rebalancing model
|
|
191
|
-
extra_kwargs = {"test": "test"}
|
|
192
|
-
|
|
193
|
-
# Combine rebalancer parameters with extra keyword arguments
|
|
194
|
-
expected_kwargs = rebalancer.parameters
|
|
195
|
-
expected_kwargs.update(extra_kwargs)
|
|
196
|
-
|
|
197
|
-
# Assert the target portfolio matches the expected output from the rebalancing model
|
|
198
|
-
assert (
|
|
199
|
-
trade_proposal._get_default_target_portfolio(**extra_kwargs) == expected_target_portfolio
|
|
200
|
-
), "We expect the target portfolio to be whatever is returned by the rebalancer model"
|
|
201
|
-
mock_fct.assert_called_once_with(
|
|
202
|
-
trade_proposal.portfolio, trade_proposal.trade_date, trade_proposal.last_effective_date, **expected_kwargs
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
# Test normalizing trades
|
|
206
|
-
def test_normalize_trades(self, trade_proposal, trade_factory):
|
|
207
|
-
"""
|
|
208
|
-
Ensure trades are normalized to sum up to 1, handling quantization errors.
|
|
209
|
-
"""
|
|
210
|
-
# Create trades for testing
|
|
211
|
-
t1 = trade_factory.create(
|
|
212
|
-
trade_proposal=trade_proposal,
|
|
213
|
-
transaction_date=trade_proposal.trade_date,
|
|
214
|
-
portfolio=trade_proposal.portfolio,
|
|
215
|
-
weighting=Decimal(0.2),
|
|
216
|
-
)
|
|
217
|
-
t2 = trade_factory.create(
|
|
218
|
-
trade_proposal=trade_proposal,
|
|
219
|
-
transaction_date=trade_proposal.trade_date,
|
|
220
|
-
portfolio=trade_proposal.portfolio,
|
|
221
|
-
weighting=Decimal(0.26),
|
|
222
|
-
)
|
|
223
|
-
t3 = trade_factory.create(
|
|
224
|
-
trade_proposal=trade_proposal,
|
|
225
|
-
transaction_date=trade_proposal.trade_date,
|
|
226
|
-
portfolio=trade_proposal.portfolio,
|
|
227
|
-
weighting=Decimal(0.14),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
# Normalize trades
|
|
231
|
-
trade_proposal.normalize_trades()
|
|
232
|
-
|
|
233
|
-
# Refresh trades from the database
|
|
234
|
-
t1.refresh_from_db()
|
|
235
|
-
t2.refresh_from_db()
|
|
236
|
-
t3.refresh_from_db()
|
|
237
|
-
|
|
238
|
-
# Expected normalized weights
|
|
239
|
-
normalized_t1_weight = Decimal("0.333333")
|
|
240
|
-
normalized_t2_weight = Decimal("0.433333")
|
|
241
|
-
normalized_t3_weight = Decimal("0.233333")
|
|
242
|
-
|
|
243
|
-
# Calculate quantization error
|
|
244
|
-
quantize_error = Decimal(1) - (normalized_t1_weight + normalized_t2_weight + normalized_t3_weight)
|
|
245
|
-
|
|
246
|
-
# Assert quantization error exists and weights are normalized correctly
|
|
247
|
-
assert quantize_error
|
|
248
|
-
assert t1.weighting == normalized_t1_weight
|
|
249
|
-
assert t2.weighting == normalized_t2_weight + quantize_error # Add quantize error to the largest position
|
|
250
|
-
assert t3.weighting == normalized_t3_weight
|
|
251
|
-
|
|
252
|
-
# Test resetting trades
|
|
253
|
-
def test_reset_trades(self, trade_proposal, instrument_factory, asset_position_factory):
|
|
254
|
-
"""
|
|
255
|
-
Verify trades are correctly reset based on effective and target portfolios.
|
|
256
|
-
"""
|
|
257
|
-
effective_date = trade_proposal.last_effective_date
|
|
258
|
-
|
|
259
|
-
# Create instruments for testing
|
|
260
|
-
i1 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
|
|
261
|
-
i2 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
|
|
262
|
-
i3 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
|
|
263
|
-
|
|
264
|
-
# Build initial effective portfolio constituting only from two positions of i1 and i2
|
|
265
|
-
asset_position_factory.create(
|
|
266
|
-
portfolio=trade_proposal.portfolio, date=effective_date, underlying_instrument=i1, weighting=Decimal("0.7")
|
|
267
|
-
)
|
|
268
|
-
asset_position_factory.create(
|
|
269
|
-
portfolio=trade_proposal.portfolio, date=effective_date, underlying_instrument=i2, weighting=Decimal("0.3")
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
# build the target portfolio
|
|
273
|
-
target_portfolio = PortfolioDTO(
|
|
274
|
-
positions=(
|
|
275
|
-
Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.4")),
|
|
276
|
-
Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.6")),
|
|
277
|
-
)
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
# Reset trades
|
|
281
|
-
trade_proposal.reset_trades(target_portfolio=target_portfolio)
|
|
282
|
-
|
|
283
|
-
# Get trades for each instrument
|
|
284
|
-
t1 = trade_proposal.trades.get(underlying_instrument=i1)
|
|
285
|
-
t2 = trade_proposal.trades.get(underlying_instrument=i2)
|
|
286
|
-
t3 = trade_proposal.trades.get(underlying_instrument=i3)
|
|
287
|
-
|
|
288
|
-
# Assert trade weights are correctly reset
|
|
289
|
-
assert t1.weighting == Decimal("-0.7")
|
|
290
|
-
assert t2.weighting == Decimal("0.1")
|
|
291
|
-
assert t3.weighting == Decimal("0.6")
|
|
292
|
-
|
|
293
|
-
# build the target portfolio
|
|
294
|
-
new_target_portfolio = PortfolioDTO(
|
|
295
|
-
positions=(
|
|
296
|
-
Position(underlying_instrument=i1.id, date=trade_proposal.trade_date, weighting=Decimal("0.2")),
|
|
297
|
-
Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.3")),
|
|
298
|
-
Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.5")),
|
|
299
|
-
)
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
trade_proposal.reset_trades(target_portfolio=new_target_portfolio)
|
|
303
|
-
# Refetch the trades for each instrument
|
|
304
|
-
t1.refresh_from_db()
|
|
305
|
-
t2.refresh_from_db()
|
|
306
|
-
t3.refresh_from_db()
|
|
307
|
-
# Assert existing trade weights are correctly updated
|
|
308
|
-
assert t1.weighting == Decimal("-0.5")
|
|
309
|
-
assert t2.weighting == Decimal("0")
|
|
310
|
-
assert t3.weighting == Decimal("0.5")
|
|
311
|
-
|
|
312
|
-
# Test replaying trade proposals
|
|
313
|
-
@patch.object(Portfolio, "drift_weights")
|
|
314
|
-
def test_replay(self, mock_fct, trade_proposal_factory):
|
|
315
|
-
"""
|
|
316
|
-
Ensure replaying trade proposals correctly calls drift_weights for each period.
|
|
317
|
-
"""
|
|
318
|
-
mock_fct.return_value = None, None
|
|
319
|
-
|
|
320
|
-
# Create approved trade proposals for testing
|
|
321
|
-
tp0 = trade_proposal_factory.create(status=TradeProposal.Status.APPROVED)
|
|
322
|
-
tp1 = trade_proposal_factory.create(
|
|
323
|
-
portfolio=tp0.portfolio,
|
|
324
|
-
status=TradeProposal.Status.APPROVED,
|
|
325
|
-
trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
|
|
326
|
-
)
|
|
327
|
-
tp2 = trade_proposal_factory.create(
|
|
328
|
-
portfolio=tp0.portfolio,
|
|
329
|
-
status=TradeProposal.Status.APPROVED,
|
|
330
|
-
trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
# Replay trade proposals
|
|
334
|
-
tp0.replay()
|
|
335
|
-
|
|
336
|
-
# Expected calls to drift_weights
|
|
337
|
-
expected_calls = [
|
|
338
|
-
call(tp0.trade_date, tp1.trade_date - timedelta(days=1)),
|
|
339
|
-
call(tp1.trade_date, tp2.trade_date - timedelta(days=1)),
|
|
340
|
-
call(tp2.trade_date, date.today()),
|
|
341
|
-
]
|
|
342
|
-
|
|
343
|
-
# Assert drift_weights was called as expected
|
|
344
|
-
mock_fct.assert_has_calls(expected_calls)
|
|
345
|
-
|
|
346
|
-
# Test stopping replay on a non-approved proposal
|
|
347
|
-
tp1.status = TradeProposal.Status.FAILED
|
|
348
|
-
tp1.save()
|
|
349
|
-
expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1))]
|
|
350
|
-
mock_fct.assert_has_calls(expected_calls)
|
|
351
|
-
|
|
352
|
-
# Test estimating shares for a trade
|
|
353
|
-
@patch.object(Portfolio, "get_total_asset_value")
|
|
354
|
-
def test_get_estimated_shares(
|
|
355
|
-
self, mock_fct, trade_proposal, trade_factory, instrument_price_factory, instrument_factory
|
|
356
|
-
):
|
|
357
|
-
"""
|
|
358
|
-
Verify shares estimation based on trade weighting and instrument price.
|
|
359
|
-
"""
|
|
360
|
-
portfolio = trade_proposal.portfolio
|
|
361
|
-
instrument = instrument_factory.create(currency=portfolio.currency)
|
|
362
|
-
trade = trade_factory.create(
|
|
363
|
-
trade_proposal=trade_proposal,
|
|
364
|
-
transaction_date=trade_proposal.trade_date,
|
|
365
|
-
portfolio=portfolio,
|
|
366
|
-
underlying_instrument=instrument,
|
|
367
|
-
)
|
|
368
|
-
trade.refresh_from_db()
|
|
369
|
-
underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=trade.transaction_date)
|
|
370
|
-
mock_fct.return_value = Decimal(1_000_000) # 1 million cash
|
|
371
|
-
|
|
372
|
-
# Assert estimated shares are correctly calculated
|
|
373
|
-
assert (
|
|
374
|
-
trade_proposal.get_estimated_shares(trade.weighting, trade.underlying_instrument)
|
|
375
|
-
== Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
@patch.object(Portfolio, "get_total_asset_value")
|
|
379
|
-
def test_get_estimated_target_cash(self, mock_fct, trade_proposal, trade_factory, cash_factory):
|
|
380
|
-
mock_fct.return_value = Decimal(1_000_000) # 1 million cash
|
|
381
|
-
cash = cash_factory.create(currency=trade_proposal.portfolio.currency)
|
|
382
|
-
trade_factory.create( # equity trade
|
|
383
|
-
trade_proposal=trade_proposal,
|
|
384
|
-
transaction_date=trade_proposal.trade_date,
|
|
385
|
-
portfolio=trade_proposal.portfolio,
|
|
386
|
-
weighting=Decimal("0.7"),
|
|
387
|
-
)
|
|
388
|
-
trade_factory.create( # cash trade
|
|
389
|
-
trade_proposal=trade_proposal,
|
|
390
|
-
transaction_date=trade_proposal.trade_date,
|
|
391
|
-
portfolio=trade_proposal.portfolio,
|
|
392
|
-
underlying_instrument=cash,
|
|
393
|
-
weighting=Decimal("0.2"),
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
target_cash_position = trade_proposal.get_estimated_target_cash(trade_proposal.portfolio.currency)
|
|
397
|
-
assert target_cash_position.weighting == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
|
|
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()
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
from wbcore.contrib.icons import WBIcon
|
|
2
|
-
from wbcore.enums import RequestType
|
|
3
|
-
from wbcore.metadata.configs import buttons as bt
|
|
4
|
-
from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TradeProposalButtonConfig(ButtonViewConfig):
|
|
8
|
-
def get_custom_list_instance_buttons(self):
|
|
9
|
-
return {
|
|
10
|
-
bt.DropDownButton(
|
|
11
|
-
label="Tools",
|
|
12
|
-
buttons=(
|
|
13
|
-
bt.ActionButton(
|
|
14
|
-
method=RequestType.PATCH,
|
|
15
|
-
identifiers=("wbportfolio:tradeproposal",),
|
|
16
|
-
key="replay",
|
|
17
|
-
icon=WBIcon.SYNCHRONIZE.icon,
|
|
18
|
-
label="Replay Trades",
|
|
19
|
-
description_fields="""
|
|
20
|
-
<p>Replay Trades. It will recompute all assets positions until next trade proposal day (or today otherwise) </p>
|
|
21
|
-
""",
|
|
22
|
-
action_label="Replay Trade",
|
|
23
|
-
title="Replay Trade",
|
|
24
|
-
),
|
|
25
|
-
bt.ActionButton(
|
|
26
|
-
method=RequestType.PATCH,
|
|
27
|
-
identifiers=("wbportfolio:tradeproposal",),
|
|
28
|
-
key="reset",
|
|
29
|
-
icon=WBIcon.REGENERATE.icon,
|
|
30
|
-
label="Reset Trades",
|
|
31
|
-
description_fields="""
|
|
32
|
-
<p>Delete and recreate initial trades to from its associated model portfolio</p>
|
|
33
|
-
""",
|
|
34
|
-
action_label="Reset Trades",
|
|
35
|
-
title="Reset Trades",
|
|
36
|
-
),
|
|
37
|
-
bt.ActionButton(
|
|
38
|
-
method=RequestType.PATCH,
|
|
39
|
-
identifiers=("wbportfolio:tradeproposal",),
|
|
40
|
-
key="normalize",
|
|
41
|
-
icon=WBIcon.EDIT.icon,
|
|
42
|
-
label="Normalize Trades",
|
|
43
|
-
description_fields="""
|
|
44
|
-
<p>Make sure all trades normalize to a total target weight of 100%</p>
|
|
45
|
-
""",
|
|
46
|
-
action_label="Normalize Trades",
|
|
47
|
-
title="Normalize Trades",
|
|
48
|
-
),
|
|
49
|
-
bt.ActionButton(
|
|
50
|
-
method=RequestType.PATCH,
|
|
51
|
-
identifiers=("wbportfolio:tradeproposal",),
|
|
52
|
-
key="deleteall",
|
|
53
|
-
icon=WBIcon.DELETE.icon,
|
|
54
|
-
label="Delete All Trades",
|
|
55
|
-
description_fields="""
|
|
56
|
-
<p>Delete all trades from this trade proposal?</p>
|
|
57
|
-
""",
|
|
58
|
-
action_label="Delete All Trades",
|
|
59
|
-
title="Delete All Trades",
|
|
60
|
-
),
|
|
61
|
-
),
|
|
62
|
-
),
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
def get_custom_instance_buttons(self):
|
|
66
|
-
return self.get_custom_list_instance_buttons()
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
from wbcore.contrib.color.enums import WBColor
|
|
4
|
-
from wbcore.metadata.configs import display as dp
|
|
5
|
-
from wbcore.metadata.configs.display.instance_display import Inline, Layout, Page
|
|
6
|
-
from wbcore.metadata.configs.display.instance_display.operators import default
|
|
7
|
-
from wbcore.metadata.configs.display.instance_display.shortcuts import Display
|
|
8
|
-
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
9
|
-
|
|
10
|
-
from wbportfolio.models import TradeProposal
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TradeProposalDisplayConfig(DisplayViewConfig):
|
|
14
|
-
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
15
|
-
return dp.ListDisplay(
|
|
16
|
-
fields=[
|
|
17
|
-
dp.Field(key="trade_date", label="Trade Date"),
|
|
18
|
-
dp.Field(key="comment", label="Comment"),
|
|
19
|
-
],
|
|
20
|
-
legends=[
|
|
21
|
-
dp.Legend(
|
|
22
|
-
key="status",
|
|
23
|
-
items=[
|
|
24
|
-
dp.LegendItem(
|
|
25
|
-
icon=WBColor.BLUE_LIGHT.value,
|
|
26
|
-
label=TradeProposal.Status.DRAFT.label,
|
|
27
|
-
value=TradeProposal.Status.DRAFT.value,
|
|
28
|
-
),
|
|
29
|
-
dp.LegendItem(
|
|
30
|
-
icon=WBColor.YELLOW_LIGHT.value,
|
|
31
|
-
label=TradeProposal.Status.SUBMIT.label,
|
|
32
|
-
value=TradeProposal.Status.SUBMIT.value,
|
|
33
|
-
),
|
|
34
|
-
dp.LegendItem(
|
|
35
|
-
icon=WBColor.GREEN_LIGHT.value,
|
|
36
|
-
label=TradeProposal.Status.APPROVED.label,
|
|
37
|
-
value=TradeProposal.Status.APPROVED.value,
|
|
38
|
-
),
|
|
39
|
-
dp.LegendItem(
|
|
40
|
-
icon=WBColor.RED_LIGHT.value,
|
|
41
|
-
label=TradeProposal.Status.DENIED.label,
|
|
42
|
-
value=TradeProposal.Status.DENIED.value,
|
|
43
|
-
),
|
|
44
|
-
],
|
|
45
|
-
),
|
|
46
|
-
],
|
|
47
|
-
formatting=[
|
|
48
|
-
dp.Formatting(
|
|
49
|
-
column="status",
|
|
50
|
-
formatting_rules=[
|
|
51
|
-
dp.FormattingRule(
|
|
52
|
-
style={"backgroundColor": WBColor.BLUE_LIGHT.value},
|
|
53
|
-
condition=("==", TradeProposal.Status.DRAFT.value),
|
|
54
|
-
),
|
|
55
|
-
dp.FormattingRule(
|
|
56
|
-
style={"backgroundColor": WBColor.YELLOW_LIGHT.value},
|
|
57
|
-
condition=("==", TradeProposal.Status.SUBMIT.value),
|
|
58
|
-
),
|
|
59
|
-
dp.FormattingRule(
|
|
60
|
-
style={"backgroundColor": WBColor.GREEN_LIGHT.value},
|
|
61
|
-
condition=("==", TradeProposal.Status.APPROVED.value),
|
|
62
|
-
),
|
|
63
|
-
dp.FormattingRule(
|
|
64
|
-
style={"backgroundColor": WBColor.RED_LIGHT.value},
|
|
65
|
-
condition=("==", TradeProposal.Status.DENIED.value),
|
|
66
|
-
),
|
|
67
|
-
],
|
|
68
|
-
)
|
|
69
|
-
],
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
def get_instance_display(self) -> Display:
|
|
73
|
-
return Display(
|
|
74
|
-
pages=[
|
|
75
|
-
Page(
|
|
76
|
-
title="Main Information",
|
|
77
|
-
layouts={
|
|
78
|
-
default(): Layout(
|
|
79
|
-
grid_template_areas=[
|
|
80
|
-
["status", "status", "status"],
|
|
81
|
-
["trade_date", "rebalancing_model", "target_portfolio"]
|
|
82
|
-
if self.view.new_mode
|
|
83
|
-
else ["trade_date", "rebalancing_model", "rebalancing_model"],
|
|
84
|
-
["comment", "comment", "comment"],
|
|
85
|
-
],
|
|
86
|
-
),
|
|
87
|
-
},
|
|
88
|
-
),
|
|
89
|
-
Page(
|
|
90
|
-
title="Trades",
|
|
91
|
-
layouts={
|
|
92
|
-
default(): Layout(
|
|
93
|
-
grid_template_areas=[["trades"]],
|
|
94
|
-
grid_template_rows=["1fr"],
|
|
95
|
-
inlines=[Inline(key="trades", endpoint="trades")],
|
|
96
|
-
),
|
|
97
|
-
},
|
|
98
|
-
),
|
|
99
|
-
]
|
|
100
|
-
)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
from wbcore.metadata.configs import display as dp
|
|
4
|
-
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
5
|
-
Display,
|
|
6
|
-
create_simple_display,
|
|
7
|
-
)
|
|
8
|
-
from wbcore.metadata.configs.display.instance_display.utils import repeat_field
|
|
9
|
-
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TransactionDisplayConfig(DisplayViewConfig):
|
|
13
|
-
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
14
|
-
return dp.ListDisplay(
|
|
15
|
-
fields=[
|
|
16
|
-
dp.Field(key="transaction_type", label="Type"),
|
|
17
|
-
dp.Field(key="transaction_underlying_type", label="Underlying Type"),
|
|
18
|
-
dp.Field(key="transaction_date", label="Transaction Date"),
|
|
19
|
-
dp.Field(key="portfolio", label="Portfolio"),
|
|
20
|
-
dp.Field(key="underlying_instrument", label="Instrument"),
|
|
21
|
-
dp.Field(key="currency_fx_rate", label="FX Rate"),
|
|
22
|
-
dp.Field(key="currency", label="Currency"),
|
|
23
|
-
dp.Field(key="total_value", label="Total Value"),
|
|
24
|
-
dp.Field(key="total_value_fx_portfolio", label="Total Value (Portfolio)"),
|
|
25
|
-
dp.Field(key="total_value_usd", label="Total Value ($)"),
|
|
26
|
-
]
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
def get_instance_display(self) -> Display:
|
|
30
|
-
return create_simple_display(
|
|
31
|
-
[
|
|
32
|
-
["transaction_type", "underlying_instrument", "external_id"],
|
|
33
|
-
["transaction_date", "book_date", "value_date"],
|
|
34
|
-
["currency", "total_value", "total_value_fx_portfolio"],
|
|
35
|
-
[".", "total_value_gross", "total_value_gross_fx_portfolio"],
|
|
36
|
-
[repeat_field(3, "comment")],
|
|
37
|
-
]
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class TransactionPortfolioDisplayConfig(TransactionDisplayConfig):
|
|
42
|
-
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
43
|
-
return dp.ListDisplay(
|
|
44
|
-
fields=[
|
|
45
|
-
dp.Field(key="transaction_type", label="Type"),
|
|
46
|
-
dp.Field(key="transaction_underlying_type", label="Underlying Type"),
|
|
47
|
-
dp.Field(key="transaction_date", label="Transaction Date"),
|
|
48
|
-
dp.Field(key="underlying_instrument", label="Instrument"),
|
|
49
|
-
dp.Field(key="currency_fx_rate", label="FX Rate"),
|
|
50
|
-
dp.Field(key="currency", label="Currency"),
|
|
51
|
-
dp.Field(key="total_value", label="Total Value"),
|
|
52
|
-
dp.Field(key="total_value_fx_portfolio", label="Total Value (Portfolio)"),
|
|
53
|
-
dp.Field(key="total_value_usd", label="Total Value ($)"),
|
|
54
|
-
]
|
|
55
|
-
)
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from rest_framework.reverse import reverse
|
|
2
|
-
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
3
|
-
|
|
4
|
-
from wbportfolio.models import TradeProposal
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TradeProposalPortfolioEndpointConfig(EndpointViewConfig):
|
|
8
|
-
def get_endpoint(self, **kwargs):
|
|
9
|
-
return reverse(
|
|
10
|
-
"wbportfolio:portfolio-tradeproposal-list", args=[self.view.kwargs["portfolio_id"]], request=self.request
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
def get_delete_endpoint(self, **kwargs):
|
|
14
|
-
if trade_proposal_id := self.view.kwargs.get("pk", None):
|
|
15
|
-
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
16
|
-
if trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
17
|
-
return reverse("wbportfolio:tradeproposal-list", args=[], request=self.request)
|
|
18
|
-
return None
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class TransactionEndpointConfig(EndpointViewConfig):
|
|
5
|
-
def get_endpoint(self, **kwargs):
|
|
6
|
-
return None
|
|
7
|
-
|
|
8
|
-
def get_instance_endpoint(self, **kwargs):
|
|
9
|
-
model = "{{transaction_url_type}}"
|
|
10
|
-
return f"{self.request.scheme}://{self.request.get_host()}/api/portfolio/{model}/"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TransactionPortfolioEndpointConfig(TransactionEndpointConfig):
|
|
14
|
-
pass
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from wbcore.menus import ItemPermission, MenuItem
|
|
2
|
-
|
|
3
|
-
from wbportfolio.permissions import is_manager
|
|
4
|
-
|
|
5
|
-
TRANSACTION_MENUITEM = MenuItem(
|
|
6
|
-
label="Transactions",
|
|
7
|
-
endpoint="wbportfolio:transaction-list",
|
|
8
|
-
permission=ItemPermission(method=is_manager, permissions=["wbportfolio.view_transaction"]),
|
|
9
|
-
)
|