wbportfolio 1.54.22__py2.py3-none-any.whl → 1.55.0__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/indexes.py +1 -1
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/rebalancing.py +1 -1
- wbportfolio/api_clients/__init__.py +0 -0
- wbportfolio/api_clients/ubs.py +150 -0
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +10 -2
- wbportfolio/factories/portfolios.py +1 -1
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/filters/assets.py +10 -2
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/order_proposals.py +58 -0
- wbportfolio/filters/portfolios.py +20 -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/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +1 -1
- wbportfolio/import_export/handlers/orders.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
- wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
- wbportfolio/import_export/parsers/ubs/equity.py +1 -1
- 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/models/asset.py +6 -20
- wbportfolio/models/builder.py +74 -31
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/orders/order_proposals.py +549 -167
- wbportfolio/models/orders/orders.py +24 -11
- wbportfolio/models/orders/routing.py +54 -0
- wbportfolio/models/portfolio.py +77 -41
- wbportfolio/models/products.py +9 -0
- wbportfolio/models/rebalancing.py +6 -6
- wbportfolio/models/transactions/transactions.py +10 -6
- wbportfolio/order_routing/__init__.py +19 -0
- wbportfolio/order_routing/adapters/__init__.py +57 -0
- wbportfolio/order_routing/adapters/ubs.py +161 -0
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/pms/typing.py +62 -8
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/equally_weighted.py +3 -3
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
- wbportfolio/rebalancing/models/model_portfolio.py +9 -10
- wbportfolio/serializers/orders/order_proposals.py +25 -21
- wbportfolio/serializers/orders/orders.py +5 -2
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/tests/conftest.py +6 -2
- wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
- wbportfolio/tests/models/test_imports.py +5 -3
- wbportfolio/tests/models/test_portfolios.py +57 -23
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
- wbportfolio/tests/rebalancing/test_models.py +3 -5
- wbportfolio/tests/signals.py +0 -10
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/viewsets/__init__.py +7 -4
- wbportfolio/viewsets/assets.py +1 -215
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +341 -155
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/menu/__init__.py +1 -0
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
- wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
- wbportfolio/viewsets/orders/order_proposals.py +47 -7
- wbportfolio/viewsets/orders/orders.py +31 -29
- wbportfolio/viewsets/portfolios.py +3 -3
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
- wbportfolio/viewsets/signals.py +0 -43
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Generated by Django 5.0.14 on 2025-08-11 14:01
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_fsm
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
def migrate_approve_to_apply(apps, schema_editor):
|
|
8
|
+
OrderProposal = apps.get_model("wbportfolio", "OrderProposal")
|
|
9
|
+
OrderProposal.objects.filter(status="APPROVED").update(status="APPLIED")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Migration(migrations.Migration):
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
('wbportfolio', '0086_orderproposal_total_cash_weight'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='product',
|
|
21
|
+
name='order_routing_custodian_adapter',
|
|
22
|
+
field=models.CharField(blank=True, help_text='The dotted path to the order routing custodian adapter', max_length=1024, null=True, verbose_name='Order Routing Custodian Adapter'),
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name='order',
|
|
26
|
+
name='execution_comment',
|
|
27
|
+
field=models.TextField(blank=True, default='', verbose_name='Execution Comment'),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name='order',
|
|
31
|
+
name='execution_confirmed',
|
|
32
|
+
field=models.BooleanField(default=False, verbose_name='Execution Confirmed'),
|
|
33
|
+
),
|
|
34
|
+
migrations.AddField(
|
|
35
|
+
model_name='orderproposal',
|
|
36
|
+
name='approver',
|
|
37
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
|
38
|
+
related_name='approver_order_proposals', to='directory.person',
|
|
39
|
+
verbose_name='Approver'),
|
|
40
|
+
),
|
|
41
|
+
migrations.AddField(
|
|
42
|
+
model_name='orderproposal',
|
|
43
|
+
name='execution_comment',
|
|
44
|
+
field=models.CharField(blank=True, default='', verbose_name='Execution Comment'),
|
|
45
|
+
),
|
|
46
|
+
migrations.AddField(
|
|
47
|
+
model_name='orderproposal',
|
|
48
|
+
name='execution_status',
|
|
49
|
+
field=models.CharField(blank=True, choices=[('PENDING', 'Pending'), ('IN_DRAFT', 'In Draft'),
|
|
50
|
+
('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled'),
|
|
51
|
+
('REJECTED', 'Rejected'), ('FAILED', 'Failed'),
|
|
52
|
+
('UNKNOWN', 'Unknown')], default='',
|
|
53
|
+
verbose_name='Execution Status'),
|
|
54
|
+
),
|
|
55
|
+
migrations.AddField(
|
|
56
|
+
model_name='orderproposal',
|
|
57
|
+
name='execution_status_detail',
|
|
58
|
+
field=models.CharField(blank=True, default='', verbose_name='Execution Status Detail'),
|
|
59
|
+
),
|
|
60
|
+
migrations.AlterField(
|
|
61
|
+
model_name='orderproposal',
|
|
62
|
+
name='status',
|
|
63
|
+
field=django_fsm.FSMField(choices=[('DRAFT', 'Draft'), ('SUBMIT', 'Pending'), ('APPROVED', 'Approved'),
|
|
64
|
+
('PENDING_EXECUTION', 'Pending Execution'), ('EXECUTED', 'Executed'),
|
|
65
|
+
('DENIED', 'Denied'), ('FAILED', 'Failed')], default='DRAFT',
|
|
66
|
+
max_length=50, verbose_name='Status'),
|
|
67
|
+
),
|
|
68
|
+
migrations.AddField(
|
|
69
|
+
model_name='index',
|
|
70
|
+
name='order_routing_custodian_adapter',
|
|
71
|
+
field=models.CharField(blank=True, help_text='The dotted path to the order routing custodian adapter',
|
|
72
|
+
max_length=1024, null=True, verbose_name='Order Routing Custodian Adapter'),
|
|
73
|
+
),
|
|
74
|
+
migrations.AddField(
|
|
75
|
+
model_name='productgroup',
|
|
76
|
+
name='order_routing_custodian_adapter',
|
|
77
|
+
field=models.CharField(blank=True, help_text='The dotted path to the order routing custodian adapter',
|
|
78
|
+
max_length=1024, null=True, verbose_name='Order Routing Custodian Adapter'),
|
|
79
|
+
),
|
|
80
|
+
migrations.RenameField(
|
|
81
|
+
model_name='rebalancer',
|
|
82
|
+
old_name='approve_order_proposal_automatically',
|
|
83
|
+
new_name='apply_order_proposal_automatically',
|
|
84
|
+
),
|
|
85
|
+
migrations.RunPython(migrate_approve_to_apply),
|
|
86
|
+
migrations.AlterField(
|
|
87
|
+
model_name='orderproposal',
|
|
88
|
+
name='status',
|
|
89
|
+
field=django_fsm.FSMField(
|
|
90
|
+
choices=[('DRAFT', 'Draft'), ('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'),
|
|
91
|
+
('APPLIED', 'Applied'), ('EXECUTION', 'Execution'), ('FAILED', 'Failed')], default='DRAFT',
|
|
92
|
+
max_length=50, verbose_name='Status'),
|
|
93
|
+
),
|
|
94
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated by Django 5.0.14 on 2025-08-18 08:34
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('wbportfolio', '0087_product_order_routing_custodian_adapter'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='orderproposal',
|
|
16
|
+
name='total_effective_portfolio_contribution',
|
|
17
|
+
field=models.DecimalField(decimal_places=16, default=Decimal('1'), max_digits=19),
|
|
18
|
+
),
|
|
19
|
+
]
|
wbportfolio/models/asset.py
CHANGED
|
@@ -30,7 +30,6 @@ from pandas._libs.tslibs.offsets import BDay
|
|
|
30
30
|
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
31
31
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
32
32
|
from wbcore.signals import pre_merge
|
|
33
|
-
from wbcore.utils.enum import ChoiceEnum
|
|
34
33
|
from wbfdm.models import Classification, ClassificationGroup, Instrument
|
|
35
34
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
36
35
|
from wbfdm.signals import add_instrument_to_investable_universe
|
|
@@ -223,25 +222,12 @@ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
|
|
|
223
222
|
)
|
|
224
223
|
|
|
225
224
|
|
|
226
|
-
class AssetPositionGroupBy(
|
|
227
|
-
INDUSTRY = "Industry"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
LIQUIDITY = "Liquidity"
|
|
233
|
-
|
|
234
|
-
@classmethod
|
|
235
|
-
def get_class_method_group_by(cls, name: str):
|
|
236
|
-
_map = {
|
|
237
|
-
"INDUSTRY": "industry",
|
|
238
|
-
"COUNTRY": AssetPosition.country_group_by,
|
|
239
|
-
"CURRENCY": AssetPosition.currency_group_by,
|
|
240
|
-
"CASH": AssetPosition.cash_group_by,
|
|
241
|
-
"MARKET_CAPITALIZATION": AssetPosition.marketcap_group_by,
|
|
242
|
-
"LIQUIDITY": AssetPosition.liquidity_group_by,
|
|
243
|
-
}
|
|
244
|
-
return _map[name]
|
|
225
|
+
class AssetPositionGroupBy(models.TextChoices):
|
|
226
|
+
INDUSTRY = "classification", "Industry"
|
|
227
|
+
INSTRUMENT_TYPE = "instrument_type", "Type"
|
|
228
|
+
COUNTRY = "country", "Country"
|
|
229
|
+
CURRENCY = "currency", "Currency"
|
|
230
|
+
CASH = "is_cash", "Cash"
|
|
245
231
|
|
|
246
232
|
|
|
247
233
|
class AssetPosition(ImportMixin, models.Model):
|
wbportfolio/models/builder.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import math
|
|
2
3
|
from collections import defaultdict
|
|
3
|
-
from contextlib import suppress
|
|
4
4
|
from datetime import date
|
|
5
|
-
from
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable
|
|
6
7
|
|
|
7
8
|
import pandas as pd
|
|
8
9
|
from celery import chain, group
|
|
@@ -20,6 +21,10 @@ if TYPE_CHECKING:
|
|
|
20
21
|
logger = logging.getLogger("pms")
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
MINIMUM_DECIMAL = 8
|
|
25
|
+
MIN_STEP = Decimal("0.00000001")
|
|
26
|
+
|
|
27
|
+
|
|
23
28
|
class AssetPositionBuilder:
|
|
24
29
|
"""
|
|
25
30
|
Efficiently converts position data into AssetPosition models with batch operations
|
|
@@ -34,7 +39,6 @@ class AssetPositionBuilder:
|
|
|
34
39
|
|
|
35
40
|
_positions: dict[date, dict[tuple[int, int | None], "AssetPosition"]]
|
|
36
41
|
|
|
37
|
-
_prices: dict[date, dict[int, float]]
|
|
38
42
|
_fx_rates: dict[date, dict[Currency, CurrencyFXRates]]
|
|
39
43
|
_instruments: dict[int, Instrument]
|
|
40
44
|
|
|
@@ -52,13 +56,24 @@ class AssetPositionBuilder:
|
|
|
52
56
|
self._change_at_date_tasks = dict()
|
|
53
57
|
self._positions = defaultdict(dict)
|
|
54
58
|
|
|
55
|
-
def get_positions(self, **kwargs):
|
|
59
|
+
def get_positions(self, fix_quantization: bool = True, **kwargs):
|
|
56
60
|
# return an iterable excluding the position with a null weight if the portfolio is manageable (otherwise, we assume the 0-weight position is valid)
|
|
57
|
-
for
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
for positions in self._positions.values():
|
|
62
|
+
quantization_weight_error = round(
|
|
63
|
+
Decimal("1") - sum(map(lambda o: o.weighting, positions.values()))
|
|
64
|
+
if fix_quantization
|
|
65
|
+
else Decimal("0"),
|
|
66
|
+
MINIMUM_DECIMAL,
|
|
67
|
+
)
|
|
68
|
+
for position in sorted(positions.values(), key=lambda x: x.weighting, reverse=True):
|
|
69
|
+
if position.weighting:
|
|
60
70
|
for k, v in kwargs.items():
|
|
61
71
|
setattr(position, k, v)
|
|
72
|
+
# if the total weight is not 100%, we add the quantization leftover to some random position (max 1e-8 per position, thus it is negligible)
|
|
73
|
+
if quantization_weight_error:
|
|
74
|
+
step = round(Decimal(math.copysign(MIN_STEP, quantization_weight_error)), MINIMUM_DECIMAL)
|
|
75
|
+
position.weighting += step
|
|
76
|
+
quantization_weight_error -= step
|
|
62
77
|
yield position
|
|
63
78
|
|
|
64
79
|
def __bool__(self) -> bool:
|
|
@@ -76,12 +91,19 @@ class AssetPositionBuilder:
|
|
|
76
91
|
try:
|
|
77
92
|
return self._fx_rates[val_date][currency]
|
|
78
93
|
except KeyError:
|
|
79
|
-
|
|
80
|
-
fx_rate = CurrencyFXRates.objects.
|
|
81
|
-
currency=currency, date=val_date
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
if currency.key == "USD":
|
|
95
|
+
fx_rate = CurrencyFXRates.objects.get_or_create(
|
|
96
|
+
currency=currency, date=val_date, defaults={"value": Decimal("1")}
|
|
97
|
+
)[0]
|
|
98
|
+
else:
|
|
99
|
+
try:
|
|
100
|
+
fx_rate = CurrencyFXRates.objects.get(
|
|
101
|
+
currency=currency, date=val_date
|
|
102
|
+
) # we create a fx rate anyway to not fail the position. The fx rate expect to be there later on
|
|
103
|
+
except CurrencyFXRates.DoesNotExist:
|
|
104
|
+
fx_rate = CurrencyFXRates.objects.filter(currency=currency, date__lt=val_date).latest("date")
|
|
105
|
+
self._fx_rates[val_date][currency] = fx_rate
|
|
106
|
+
return fx_rate
|
|
85
107
|
|
|
86
108
|
def _get_price(self, val_date: date, instrument: Instrument) -> float | None:
|
|
87
109
|
try:
|
|
@@ -94,7 +116,6 @@ class AssetPositionBuilder:
|
|
|
94
116
|
val_date: date,
|
|
95
117
|
instrument_id: int,
|
|
96
118
|
weighting: float,
|
|
97
|
-
infer_underlying_quote_price: bool = False,
|
|
98
119
|
**kwargs,
|
|
99
120
|
) -> "AssetPosition":
|
|
100
121
|
underlying_quote = self._get_instrument(instrument_id)
|
|
@@ -104,7 +125,7 @@ class AssetPositionBuilder:
|
|
|
104
125
|
|
|
105
126
|
parameters = dict(
|
|
106
127
|
underlying_quote=underlying_quote,
|
|
107
|
-
weighting=round(weighting,
|
|
128
|
+
weighting=round(weighting, MINIMUM_DECIMAL),
|
|
108
129
|
date=val_date,
|
|
109
130
|
asset_valuation_date=val_date,
|
|
110
131
|
is_estimated=True,
|
|
@@ -119,21 +140,38 @@ class AssetPositionBuilder:
|
|
|
119
140
|
)
|
|
120
141
|
parameters.update(kwargs)
|
|
121
142
|
position = AssetPosition(**parameters)
|
|
122
|
-
position.pre_save(
|
|
123
|
-
infer_underlying_quote_price=infer_underlying_quote_price
|
|
124
|
-
) # 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
|
|
125
143
|
return position
|
|
126
144
|
|
|
127
|
-
def
|
|
128
|
-
self.returns
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
from_date
|
|
136
|
-
|
|
145
|
+
def load_returns(self, instrument_ids: Iterable[int], from_date: date, to_date: date, use_dl: bool = True):
|
|
146
|
+
if self.returns.empty:
|
|
147
|
+
self.prices, self.returns = Instrument.objects.filter(id__in=instrument_ids).get_returns_df(
|
|
148
|
+
from_date=from_date, to_date=to_date, to_currency=self.portfolio.currency, use_dl=use_dl
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
min_date = min(self.prices.keys())
|
|
152
|
+
max_date = max(self.prices.keys())
|
|
153
|
+
if from_date < min_date or to_date > max_date:
|
|
154
|
+
# we need to refetch everything as we are missing index
|
|
155
|
+
self.prices, self.returns = Instrument.objects.filter(
|
|
156
|
+
id__in=set(instrument_ids).union(set(self.returns.columns))
|
|
157
|
+
).get_returns_df(
|
|
158
|
+
from_date=min(from_date, min_date),
|
|
159
|
+
to_date=max(to_date, max_date),
|
|
160
|
+
to_currency=self.portfolio.currency,
|
|
161
|
+
use_dl=use_dl,
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
instruments = set(instrument_ids) - set(self.returns.columns)
|
|
165
|
+
if instruments:
|
|
166
|
+
new_prices, new_returns = Instrument.objects.filter(id__in=instruments).get_returns_df(
|
|
167
|
+
from_date=min(from_date, min_date),
|
|
168
|
+
to_date=max(to_date, max_date),
|
|
169
|
+
to_currency=self.portfolio.currency,
|
|
170
|
+
use_dl=use_dl,
|
|
171
|
+
)
|
|
172
|
+
self.returns = self.returns.join(new_returns, how="left").fillna(0)
|
|
173
|
+
for d, p in new_prices.items():
|
|
174
|
+
self.prices[d].update(p)
|
|
137
175
|
|
|
138
176
|
def add(
|
|
139
177
|
self,
|
|
@@ -151,8 +189,10 @@ class AssetPositionBuilder:
|
|
|
151
189
|
positions = [(val_date, i, w) for i, w in positions[1].items()] # unflatten data to make it iterable
|
|
152
190
|
for position in positions:
|
|
153
191
|
if not isinstance(position, AssetPosition):
|
|
154
|
-
position = self._dict_to_model(*position
|
|
155
|
-
|
|
192
|
+
position = self._dict_to_model(*position)
|
|
193
|
+
position.pre_save(
|
|
194
|
+
infer_underlying_quote_price=infer_underlying_quote_price
|
|
195
|
+
) # 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
|
|
156
196
|
# Generate unique composite key
|
|
157
197
|
key = (
|
|
158
198
|
position.underlying_quote.id,
|
|
@@ -165,6 +205,9 @@ class AssetPositionBuilder:
|
|
|
165
205
|
position.initial_shares += existing_position.initial_shares
|
|
166
206
|
# ensure the position portfolio is the iterator portfolio (could be different when computing look-through for instance)
|
|
167
207
|
position.portfolio = self.portfolio
|
|
208
|
+
position.weighting = Decimal(
|
|
209
|
+
round(position.weighting, 8)
|
|
210
|
+
) # set the weight as it will be saved in the db to handle quantization error accordingly
|
|
168
211
|
if position.initial_price is not None and position.initial_currency_fx_rate is not None:
|
|
169
212
|
self._positions[position.date][key] = position
|
|
170
213
|
return self
|
|
@@ -248,7 +291,7 @@ class AssetPositionBuilder:
|
|
|
248
291
|
tasks = chain(
|
|
249
292
|
*[
|
|
250
293
|
trigger_portfolio_change_as_task.si(
|
|
251
|
-
self.portfolio.id, d, changed_portfolio=portfolio, **task_kwargs
|
|
294
|
+
self.portfolio.id, d, changed_portfolio=portfolio, evaluate_rebalancer=False, **task_kwargs
|
|
252
295
|
)
|
|
253
296
|
for d, portfolio in self._change_at_date_tasks.items()
|
|
254
297
|
]
|
|
@@ -150,6 +150,13 @@ class PMSInstrumentAbstractModel(PMSInstrument):
|
|
|
150
150
|
default="wbportfolio.models.portfolio.default_estimate_net_value",
|
|
151
151
|
verbose_name="NAV Computation Method",
|
|
152
152
|
)
|
|
153
|
+
order_routing_custodian_adapter = models.CharField(
|
|
154
|
+
blank=True,
|
|
155
|
+
null=True,
|
|
156
|
+
max_length=1024,
|
|
157
|
+
verbose_name="Order Routing Custodian Adapter",
|
|
158
|
+
help_text="The dotted path to the order routing custodian adapter",
|
|
159
|
+
)
|
|
153
160
|
risk_scale = models.IntegerField(
|
|
154
161
|
validators=[MinValueValidator(1), MaxValueValidator(7)],
|
|
155
162
|
default=4,
|