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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Generated by Django 5.0.12 on 2025-11-11 10:16
|
|
2
|
+
|
|
3
|
+
import django_fsm
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
import django_fsm
|
|
8
|
+
from django.db import migrations
|
|
9
|
+
|
|
10
|
+
def migrate_status(apps, schema_editor):
|
|
11
|
+
OrderProposal = apps.get_model("wbportfolio", "OrderProposal")
|
|
12
|
+
OrderProposal.objects.filter(status="APPLIED").update(status="CONFIRMED")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Migration(migrations.Migration):
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
('wbportfolio', '0091_remove_order_execution_confirmed_and_more'),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
operations = [
|
|
22
|
+
migrations.AddField(
|
|
23
|
+
model_name='order',
|
|
24
|
+
name='quantization_error',
|
|
25
|
+
field=models.DecimalField(decimal_places=8, default=Decimal('0'), max_digits=9, verbose_name='Quantization Error'),
|
|
26
|
+
),
|
|
27
|
+
migrations.AlterField(
|
|
28
|
+
model_name='orderproposal',
|
|
29
|
+
name='status',
|
|
30
|
+
field=django_fsm.FSMField(choices=[('DRAFT', 'Draft'), ('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'), ('EXECUTION', 'Execution'), ('APPLIED', 'Applied'), ('FAILED', 'Failed')], default='DRAFT', max_length=50, verbose_name='Status'),
|
|
31
|
+
),
|
|
32
|
+
migrations.AlterField(
|
|
33
|
+
model_name='orderproposal',
|
|
34
|
+
name='status',
|
|
35
|
+
field=django_fsm.FSMField(
|
|
36
|
+
choices=[('DRAFT', 'Draft'), ('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'),
|
|
37
|
+
('EXECUTION', 'Execution'), ('CONFIRMED', 'Confirmed'), ('FAILED', 'Failed')], default='DRAFT',
|
|
38
|
+
max_length=50, verbose_name='Status'),
|
|
39
|
+
),
|
|
40
|
+
migrations.AddField(
|
|
41
|
+
model_name='order',
|
|
42
|
+
name='execution_trade',
|
|
43
|
+
field=models.OneToOneField(blank=True, help_text='The executed Trade', null=True,
|
|
44
|
+
on_delete=django.db.models.deletion.SET_NULL, related_name='order',
|
|
45
|
+
to='wbportfolio.trade'),
|
|
46
|
+
),
|
|
47
|
+
migrations.RunPython(migrate_status),
|
|
48
|
+
|
|
49
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by Django 5.0.12 on 2025-11-28 13:11
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
def handle_data(apps, schema_editor):
|
|
6
|
+
PortfolioPortfolioThroughModel = apps.get_model('wbportfolio', 'PortfolioPortfolioThroughModel')
|
|
7
|
+
PortfolioPortfolioThroughModel.objects.filter(type="PRIMARY").update(type="LOOK_THROUGH")
|
|
8
|
+
from wbportfolio.models.portfolio import Portfolio
|
|
9
|
+
for portfolio in Portfolio.objects.all():
|
|
10
|
+
if portfolio.assets.exists():
|
|
11
|
+
val_date = portfolio.assets.latest("date").date
|
|
12
|
+
for parent_ptf, _ in portfolio.get_parent_portfolios(val_date):
|
|
13
|
+
PortfolioPortfolioThroughModel.objects.get_or_create(portfolio_id=portfolio.id, dependency_portfolio_id=parent_ptf.id, defaults={"type": "HIERARCHICAL"})
|
|
14
|
+
class Migration(migrations.Migration):
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
('wbportfolio', '0092_order_quantization_error_alter_orderproposal_status'),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
operations = [
|
|
21
|
+
migrations.RemoveConstraint(
|
|
22
|
+
model_name='portfolioportfoliothroughmodel',
|
|
23
|
+
name='unique_primary',
|
|
24
|
+
),
|
|
25
|
+
migrations.AlterField(
|
|
26
|
+
model_name='portfolioportfoliothroughmodel',
|
|
27
|
+
name='type',
|
|
28
|
+
field=models.CharField(choices=[('LOOK_THROUGH', 'Look-through'), ('MODEL', 'Model'), ('CUSTODIAN', 'Custodian'), ('HIERARCHICAL', 'Hierarchical')], default='LOOK_THROUGH', verbose_name='Type'),
|
|
29
|
+
),
|
|
30
|
+
migrations.AddConstraint(
|
|
31
|
+
model_name='portfolioportfoliothroughmodel',
|
|
32
|
+
constraint=models.UniqueConstraint(condition=models.Q(('type', 'LOOK_THROUGH')), fields=('portfolio', 'type'), name='unique_lookthrough'),
|
|
33
|
+
),
|
|
34
|
+
migrations.RunPython(handle_data)
|
|
35
|
+
]
|
wbportfolio/models/__init__.py
CHANGED
|
@@ -17,5 +17,7 @@ from .portfolio_cash_flow import DailyPortfolioCashFlow
|
|
|
17
17
|
from .portfolio_swing_pricings import PortfolioSwingPricing
|
|
18
18
|
from .registers import Register
|
|
19
19
|
from .transactions import *
|
|
20
|
+
from .orders import *
|
|
20
21
|
from .reconciliations import AccountReconciliation, AccountReconciliationLine
|
|
21
22
|
from .signals import *
|
|
23
|
+
from .rebalancing import RebalancingModel, Rebalancer
|
|
@@ -212,7 +212,7 @@ def post_adjustment_on_prices(adjustment_id, automatically_confirm_approve_adjus
|
|
|
212
212
|
adjustment.apply_adjustment_on_assets()
|
|
213
213
|
adjustment.status = Adjustment.Status.APPLIED
|
|
214
214
|
else:
|
|
215
|
-
for user in User.objects.filter(profile__in=PortfolioRole.portfolio_managers()):
|
|
215
|
+
for user in User.objects.filter(profile__in=PortfolioRole.portfolio_managers(), is_active=True):
|
|
216
216
|
send_notification(
|
|
217
217
|
code="wbportfolio.adjustment.add",
|
|
218
218
|
title="A new adjustment was imported",
|
wbportfolio/models/asset.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
from contextlib import suppress
|
|
3
3
|
from datetime import date
|
|
4
4
|
from decimal import Decimal, InvalidOperation
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from django.contrib import admin
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
8
9
|
from django.db import models
|
|
9
10
|
from django.db.models import (
|
|
10
11
|
Case,
|
|
@@ -30,7 +31,6 @@ from pandas._libs.tslibs.offsets import BDay
|
|
|
30
31
|
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
31
32
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
32
33
|
from wbcore.signals import pre_merge
|
|
33
|
-
from wbcore.utils.enum import ChoiceEnum
|
|
34
34
|
from wbfdm.models import Classification, ClassificationGroup, Instrument
|
|
35
35
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
36
36
|
from wbfdm.signals import add_instrument_to_investable_universe
|
|
@@ -43,6 +43,8 @@ from wbportfolio.models.portfolio_relationship import (
|
|
|
43
43
|
from wbportfolio.models.roles import PortfolioRole
|
|
44
44
|
from wbportfolio.pms.typing import Position as PositionDTO
|
|
45
45
|
|
|
46
|
+
logger = logging.getLogger("pms")
|
|
47
|
+
|
|
46
48
|
MARKETCAP_S = 2_000_000_000
|
|
47
49
|
MARKETCAP_M = 10_000_000_000
|
|
48
50
|
MARKETCAP_L = 50_000_000_000
|
|
@@ -55,145 +57,7 @@ HOUR = MINUTE * 60
|
|
|
55
57
|
DAY = HOUR * 24
|
|
56
58
|
|
|
57
59
|
if TYPE_CHECKING:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class AssetPositionIterator:
|
|
62
|
-
"""
|
|
63
|
-
Efficiently converts position data into AssetPosition models with batch operations
|
|
64
|
-
and proper dependency management.
|
|
65
|
-
|
|
66
|
-
Features:
|
|
67
|
-
- Bulk database fetching for performance
|
|
68
|
-
- Thread-safe operations
|
|
69
|
-
- Clear type hints
|
|
70
|
-
- Memory-efficient storage
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
positions: dict[tuple[date, int, int | None], "AssetPosition"]
|
|
74
|
-
|
|
75
|
-
_prices: dict[date, dict[int, float]]
|
|
76
|
-
_weights: dict[date, dict[int, float]]
|
|
77
|
-
_fx_rates: dict[date, dict[Currency, CurrencyFXRates]]
|
|
78
|
-
_instruments: dict[int, Instrument]
|
|
79
|
-
|
|
80
|
-
def __init__(
|
|
81
|
-
self,
|
|
82
|
-
portfolio: "Portfolio",
|
|
83
|
-
prices: dict[date, dict[int, float]] | None = None,
|
|
84
|
-
infer_underlying_quote_price: bool = False,
|
|
85
|
-
):
|
|
86
|
-
self.portfolio = portfolio
|
|
87
|
-
self.infer_underlying_quote_price = infer_underlying_quote_price
|
|
88
|
-
# Initialize data stores with type hints
|
|
89
|
-
self._instruments = {}
|
|
90
|
-
self._fx_rates = defaultdict(dict)
|
|
91
|
-
self._weights = defaultdict(dict)
|
|
92
|
-
self._prices = prices or defaultdict(dict)
|
|
93
|
-
self.positions = dict()
|
|
94
|
-
|
|
95
|
-
def _get_instrument(self, instrument_id: int) -> Instrument:
|
|
96
|
-
try:
|
|
97
|
-
return self._instruments[instrument_id]
|
|
98
|
-
except KeyError:
|
|
99
|
-
instrument = Instrument.objects.get(id=instrument_id)
|
|
100
|
-
self._instruments[instrument_id] = instrument
|
|
101
|
-
return instrument
|
|
102
|
-
|
|
103
|
-
def _get_fx_rate(self, val_date: date, currency: Currency) -> CurrencyFXRates | None:
|
|
104
|
-
try:
|
|
105
|
-
return self._fx_rates[val_date][currency]
|
|
106
|
-
except KeyError:
|
|
107
|
-
with suppress(CurrencyFXRates.DoesNotExist):
|
|
108
|
-
fx_rate = CurrencyFXRates.objects.get(
|
|
109
|
-
currency=currency, date=val_date
|
|
110
|
-
) # we create a fx rate anyway to not fail the position. The fx rate expect to be there later on
|
|
111
|
-
self._fx_rates[val_date][currency] = fx_rate
|
|
112
|
-
return fx_rate
|
|
113
|
-
|
|
114
|
-
def _get_price(self, val_date: date, instrument: Instrument) -> float | None:
|
|
115
|
-
try:
|
|
116
|
-
return self._prices[val_date][instrument.id]
|
|
117
|
-
except KeyError:
|
|
118
|
-
return None
|
|
119
|
-
|
|
120
|
-
def _dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> "AssetPosition":
|
|
121
|
-
underlying_quote = self._get_instrument(instrument_id)
|
|
122
|
-
currency_fx_rate_portfolio_to_usd = self._get_fx_rate(val_date, self.portfolio.currency)
|
|
123
|
-
currency_fx_rate_instrument_to_usd = self._get_fx_rate(val_date, underlying_quote.currency)
|
|
124
|
-
price = self._get_price(val_date, underlying_quote)
|
|
125
|
-
position = AssetPosition(
|
|
126
|
-
underlying_quote=underlying_quote,
|
|
127
|
-
weighting=weighting,
|
|
128
|
-
date=val_date,
|
|
129
|
-
asset_valuation_date=val_date,
|
|
130
|
-
is_estimated=True,
|
|
131
|
-
portfolio=self.portfolio,
|
|
132
|
-
currency=underlying_quote.currency,
|
|
133
|
-
initial_price=price,
|
|
134
|
-
currency_fx_rate_portfolio_to_usd=currency_fx_rate_portfolio_to_usd,
|
|
135
|
-
currency_fx_rate_instrument_to_usd=currency_fx_rate_instrument_to_usd,
|
|
136
|
-
initial_currency_fx_rate=None,
|
|
137
|
-
underlying_quote_price=None,
|
|
138
|
-
underlying_instrument=None,
|
|
139
|
-
)
|
|
140
|
-
position.pre_save(
|
|
141
|
-
infer_underlying_quote_price=self.infer_underlying_quote_price
|
|
142
|
-
) # inferring underlying quote price is potentially very slow for big dataset of positions, it's not very needed for model portfolio so we disable it
|
|
143
|
-
return position
|
|
144
|
-
|
|
145
|
-
def add(
|
|
146
|
-
self,
|
|
147
|
-
positions: list["AssetPosition"] | tuple[date, dict[int, float]],
|
|
148
|
-
):
|
|
149
|
-
"""
|
|
150
|
-
Add multiple positions efficiently with batch processing
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
positions: Iterable of AssetPosition instances or dictionary of weight {instrument_id: weight} that needs to be converted into AssetPosition
|
|
154
|
-
"""
|
|
155
|
-
if isinstance(positions, tuple):
|
|
156
|
-
val_date = positions[0]
|
|
157
|
-
positions = [(val_date, i, w) for i, w in positions[1].items()] # unflatten data to make it iterable
|
|
158
|
-
for position in positions:
|
|
159
|
-
if not isinstance(position, AssetPosition):
|
|
160
|
-
position = self._dict_to_model(*position)
|
|
161
|
-
|
|
162
|
-
# Generate unique composite key
|
|
163
|
-
key = (
|
|
164
|
-
position.date,
|
|
165
|
-
position.underlying_quote.id,
|
|
166
|
-
position.portfolio_created.id if position.portfolio_created else None,
|
|
167
|
-
)
|
|
168
|
-
# Merge duplicate positions
|
|
169
|
-
if existing_position := self.positions.get(key):
|
|
170
|
-
position.weighting += existing_position.weighting
|
|
171
|
-
position.initial_shares += existing_position.initial_shares
|
|
172
|
-
# ensure the position portfolio is the iterator portfolio (could be different when computing look-through for instance)
|
|
173
|
-
position.portfolio = self.portfolio
|
|
174
|
-
if position.initial_price is not None and position.initial_currency_fx_rate is not None:
|
|
175
|
-
self.positions[key] = position
|
|
176
|
-
self._weights[position.date][position.underlying_quote.id] = float(position.weighting)
|
|
177
|
-
|
|
178
|
-
return self
|
|
179
|
-
|
|
180
|
-
def get_dates(self) -> list[date]:
|
|
181
|
-
"""Get sorted list of unique dates"""
|
|
182
|
-
return list(self._weights.keys())
|
|
183
|
-
|
|
184
|
-
def get_weights(self) -> dict[date, dict[int, float]]:
|
|
185
|
-
"""Get weight structure with instrument IDs as keys"""
|
|
186
|
-
return dict(self._weights)
|
|
187
|
-
|
|
188
|
-
def __iter__(self):
|
|
189
|
-
# return an iterable excluding the position with a null weight if the portfolio is manageable (otherwise, we assume the 0-weight position is valid)
|
|
190
|
-
yield from filter(lambda a: not a.portfolio.is_manageable or a.weighting, self.positions.values())
|
|
191
|
-
|
|
192
|
-
def __getitem__(self, item: tuple[date, Instrument]) -> float:
|
|
193
|
-
return self._weights[item[0]][item[1].id]
|
|
194
|
-
|
|
195
|
-
def __bool__(self) -> bool:
|
|
196
|
-
return len(self.positions.keys()) > 0
|
|
60
|
+
pass
|
|
197
61
|
|
|
198
62
|
|
|
199
63
|
class AssetPositionDefaultQueryset(QuerySet):
|
|
@@ -359,25 +223,12 @@ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
|
|
|
359
223
|
)
|
|
360
224
|
|
|
361
225
|
|
|
362
|
-
class AssetPositionGroupBy(
|
|
363
|
-
INDUSTRY = "Industry"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
LIQUIDITY = "Liquidity"
|
|
369
|
-
|
|
370
|
-
@classmethod
|
|
371
|
-
def get_class_method_group_by(cls, name: str):
|
|
372
|
-
_map = {
|
|
373
|
-
"INDUSTRY": "industry",
|
|
374
|
-
"COUNTRY": AssetPosition.country_group_by,
|
|
375
|
-
"CURRENCY": AssetPosition.currency_group_by,
|
|
376
|
-
"CASH": AssetPosition.cash_group_by,
|
|
377
|
-
"MARKET_CAPITALIZATION": AssetPosition.marketcap_group_by,
|
|
378
|
-
"LIQUIDITY": AssetPosition.liquidity_group_by,
|
|
379
|
-
}
|
|
380
|
-
return _map[name]
|
|
226
|
+
class AssetPositionGroupBy(models.TextChoices):
|
|
227
|
+
INDUSTRY = "classification", "Industry"
|
|
228
|
+
INSTRUMENT_TYPE = "instrument_type", "Type"
|
|
229
|
+
COUNTRY = "country", "Country"
|
|
230
|
+
CURRENCY = "currency", "Currency"
|
|
231
|
+
CASH = "is_cash", "Cash"
|
|
381
232
|
|
|
382
233
|
|
|
383
234
|
class AssetPosition(ImportMixin, models.Model):
|
|
@@ -464,8 +315,8 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
464
315
|
# )
|
|
465
316
|
|
|
466
317
|
weighting = models.DecimalField(
|
|
467
|
-
decimal_places=
|
|
468
|
-
max_digits=
|
|
318
|
+
decimal_places=8,
|
|
319
|
+
max_digits=9,
|
|
469
320
|
default=Decimal(0),
|
|
470
321
|
verbose_name="Weight",
|
|
471
322
|
help_text="The Weight of the Asset on the price date of the Asset.",
|
|
@@ -558,7 +409,7 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
558
409
|
analytical_objects = AnalyticalAssetPositionManager()
|
|
559
410
|
unannotated_objects = models.Manager()
|
|
560
411
|
|
|
561
|
-
def pre_save(
|
|
412
|
+
def pre_save( # noqa: C901
|
|
562
413
|
self, create_underlying_quote_price_if_missing: bool = False, infer_underlying_quote_price: bool = True
|
|
563
414
|
):
|
|
564
415
|
if not self.asset_valuation_date:
|
|
@@ -579,12 +430,12 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
579
430
|
):
|
|
580
431
|
try:
|
|
581
432
|
self.underlying_quote = self.underlying_instrument.children.get(is_primary=True)
|
|
582
|
-
except:
|
|
433
|
+
except ObjectDoesNotExist:
|
|
583
434
|
self.underlying_quote = self.underlying_instrument
|
|
584
435
|
|
|
585
436
|
if not getattr(self, "currency", None):
|
|
586
437
|
self.currency = self.underlying_quote.currency
|
|
587
|
-
if not self.underlying_quote_price and infer_underlying_quote_price:
|
|
438
|
+
if not self.underlying_quote_price and (infer_underlying_quote_price or not self.initial_price):
|
|
588
439
|
try:
|
|
589
440
|
# We get only the instrument price (and don't create it) because we don't want to create product instrument price on asset position propagation
|
|
590
441
|
# Instead, we decided to opt for a post_save based system that will assign the missing position price when a price is created
|
|
@@ -597,7 +448,10 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
597
448
|
net_value = self.initial_price
|
|
598
449
|
# in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
|
|
599
450
|
if self.currency != self.underlying_quote.currency:
|
|
600
|
-
|
|
451
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
452
|
+
net_value *= self.currency.convert(
|
|
453
|
+
self.asset_valuation_date, self.underlying_quote.currency
|
|
454
|
+
)
|
|
601
455
|
self.underlying_quote_price = InstrumentPrice.objects.create(
|
|
602
456
|
calculated=False,
|
|
603
457
|
instrument=self.underlying_quote,
|
|
@@ -652,6 +506,8 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
652
506
|
portfolio_created=self.portfolio_created,
|
|
653
507
|
)
|
|
654
508
|
self.initial_shares = previous_pos.initial_shares
|
|
509
|
+
if self.underlying_quote:
|
|
510
|
+
self.exchange = self.underlying_quote.exchange
|
|
655
511
|
|
|
656
512
|
def save(self, *args, create_underlying_quote_price_if_missing: bool = False, **kwargs):
|
|
657
513
|
self.pre_save(create_underlying_quote_price_if_missing=create_underlying_quote_price_if_missing)
|
|
@@ -691,22 +547,22 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
691
547
|
def get_portfolio_total_asset_value(self) -> Decimal:
|
|
692
548
|
return self.portfolio.get_total_asset_value(self.date)
|
|
693
549
|
|
|
694
|
-
def _build_dto(self,
|
|
550
|
+
def _build_dto(self, **kwargs) -> PositionDTO:
|
|
695
551
|
"""
|
|
696
552
|
Data Transfer Object
|
|
697
553
|
Returns:
|
|
698
554
|
DTO position object
|
|
699
555
|
"""
|
|
700
|
-
|
|
556
|
+
parameters = dict(
|
|
701
557
|
underlying_instrument=self.underlying_quote.id,
|
|
702
|
-
weighting=self.weighting
|
|
558
|
+
weighting=self.weighting,
|
|
703
559
|
shares=self._shares,
|
|
704
560
|
date=self.date,
|
|
705
561
|
asset_valuation_date=self.asset_valuation_date,
|
|
706
562
|
instrument_type=self.underlying_quote.security_instrument_type.id,
|
|
707
563
|
currency=self.underlying_quote.currency.id,
|
|
708
564
|
country=self.underlying_quote.country.id if self.underlying_quote.country else None,
|
|
709
|
-
is_cash=self.underlying_quote.is_cash,
|
|
565
|
+
is_cash=self.underlying_quote.is_cash or self.underlying_quote.is_cash_equivalent,
|
|
710
566
|
primary_classification=(
|
|
711
567
|
self.underlying_quote.primary_classification.id
|
|
712
568
|
if self.underlying_quote.primary_classification
|
|
@@ -725,6 +581,8 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
725
581
|
currency_fx_rate=self._currency_fx_rate,
|
|
726
582
|
portfolio_created=self.portfolio_created.id if self.portfolio_created else None,
|
|
727
583
|
)
|
|
584
|
+
parameters.update(kwargs)
|
|
585
|
+
return PositionDTO(**parameters)
|
|
728
586
|
|
|
729
587
|
@cached_property
|
|
730
588
|
@admin.display(description="Adjusting Factor (adjustment)")
|