wbportfolio 1.55.10rc0__py2.py3-none-any.whl → 1.56.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/filters/assets.py +0 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/fees.py +0 -2
- wbportfolio/filters/transactions/trades.py +0 -1
- wbportfolio/import_export/handlers/trade.py +1 -1
- wbportfolio/import_export/parsers/natixis/equity.py +21 -3
- wbportfolio/import_export/parsers/natixis/utils.py +13 -23
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/models/asset.py +1 -1
- wbportfolio/models/builder.py +0 -1
- wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
- wbportfolio/models/orders/order_proposals.py +9 -6
- wbportfolio/models/orders/orders.py +84 -62
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/trades.py +1 -0
- wbportfolio/models/transactions/transactions.py +16 -4
- wbportfolio/risk_management/backends/exposure_portfolio.py +2 -2
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/serializers/orders/orders.py +56 -23
- wbportfolio/tests/models/orders/test_order_proposals.py +27 -7
- wbportfolio/viewsets/charts/assets.py +2 -0
- wbportfolio/viewsets/orders/orders.py +22 -4
- {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.0.dist-info}/RECORD +26 -25
- {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.0.dist-info}/licenses/LICENSE +0 -0
wbportfolio/filters/assets.py
CHANGED
|
@@ -456,7 +456,6 @@ def get_default_hedged_currency(field, request, view):
|
|
|
456
456
|
|
|
457
457
|
class ContributionChartFilter(wb_filters.FilterSet):
|
|
458
458
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
459
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
460
459
|
label="Date Range",
|
|
461
460
|
required=True,
|
|
462
461
|
clearable=False,
|
wbportfolio/filters/positions.py
CHANGED
|
@@ -76,7 +76,6 @@ class AssetPositionPandasFilter(DateFilterMixin, PandasFilterSetMixin, wb_filter
|
|
|
76
76
|
date = total_value_fx_usd = total_value_fx_usd__gte = total_value_fx_usd__lte = None
|
|
77
77
|
|
|
78
78
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
79
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
80
79
|
label="Date Range",
|
|
81
80
|
required=True,
|
|
82
81
|
clearable=False,
|
|
@@ -16,7 +16,6 @@ class FeesFilter(wb_filters.FilterSet):
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
fee_date = wb_filters.DateRangeFilter(
|
|
19
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
20
19
|
label="Date Range",
|
|
21
20
|
initial=get_transaction_default_date_range,
|
|
22
21
|
required=True,
|
|
@@ -52,7 +51,6 @@ class FeesProductFilterSet(FeesFilter):
|
|
|
52
51
|
class FeesAggregatedFilter(PandasFilterSetMixin, wb_filters.FilterSet):
|
|
53
52
|
fee_date = wb_filters.DateRangeFilter(
|
|
54
53
|
label="Date Range",
|
|
55
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
56
54
|
required=True,
|
|
57
55
|
clearable=False,
|
|
58
56
|
initial=current_quarter_date_range,
|
|
@@ -14,7 +14,6 @@ from .utils import get_transaction_default_date_range
|
|
|
14
14
|
|
|
15
15
|
class TradeFilter(OppositeSharesFieldMethodMixin, wb_filters.FilterSet):
|
|
16
16
|
transaction_date = wb_filters.DateRangeFilter(
|
|
17
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
18
17
|
label="Date Range",
|
|
19
18
|
initial=get_transaction_default_date_range,
|
|
20
19
|
)
|
|
@@ -86,7 +86,7 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
86
86
|
data["transaction_date"] = data["book_date"]
|
|
87
87
|
return self.model.objects.create(**data, import_source=self.import_source)
|
|
88
88
|
|
|
89
|
-
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
|
|
89
|
+
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model: # noqa: C901
|
|
90
90
|
self.import_source.log += "\nGet Trade Instance."
|
|
91
91
|
if transaction_date := data.get("transaction_date"):
|
|
92
92
|
dates_lookup = {"transaction_date": transaction_date}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from zipfile import BadZipFile
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
|
|
@@ -29,13 +31,26 @@ def _apply_adjusting_factor(row):
|
|
|
29
31
|
def parse(import_source):
|
|
30
32
|
# Parse the Parts of the filename into the different parts
|
|
31
33
|
parts = file_name_parse_isin(import_source.file.name)
|
|
32
|
-
|
|
33
34
|
# Get the valuation date and investment from the parts list
|
|
34
35
|
valuation_date = parts["valuation_date"]
|
|
35
36
|
product_data = parts["product"]
|
|
36
37
|
|
|
37
38
|
# Load file into a CSV DictReader
|
|
38
|
-
|
|
39
|
+
if import_source.file.name.lower().endswith(".csv"):
|
|
40
|
+
df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=";")
|
|
41
|
+
else:
|
|
42
|
+
try:
|
|
43
|
+
df = pd.read_excel(import_source.file, engine="openpyxl", sheet_name="Basket Valuation")
|
|
44
|
+
except BadZipFile:
|
|
45
|
+
df = pd.read_excel(import_source.file, engine="xlrd", sheet_name="Basket Valuation")
|
|
46
|
+
xx, yy = np.where(df == "Ticker")
|
|
47
|
+
if not xx and not yy:
|
|
48
|
+
xx, yy = np.where(df == "Code")
|
|
49
|
+
df = df.iloc[xx[0] :, yy[0] :]
|
|
50
|
+
df = df.rename(columns=df.iloc[0]).drop(df.index[0]).dropna(how="all")
|
|
51
|
+
df["Quotity/Adj. factor"] = 1.0
|
|
52
|
+
df = df.rename(columns={"Code": "Ticker"})
|
|
53
|
+
|
|
39
54
|
df = df.rename(columns=FIELD_MAP)
|
|
40
55
|
df = df.dropna(subset=["initial_price"])
|
|
41
56
|
df["initial_price"] = df["initial_price"].astype("str").str.replace(" ", "").astype("float")
|
|
@@ -50,7 +65,10 @@ def parse(import_source):
|
|
|
50
65
|
df = df.drop(columns=df.columns.difference(FIELD_MAP.values()))
|
|
51
66
|
|
|
52
67
|
df["portfolio__instrument_type"] = "product"
|
|
53
|
-
|
|
68
|
+
if "isin" in product_data:
|
|
69
|
+
df["portfolio__isin"] = product_data["isin"]
|
|
70
|
+
if "ticker" in product_data:
|
|
71
|
+
df["portfolio__ticker"] = product_data["ticker"]
|
|
54
72
|
df["is_estimated"] = False
|
|
55
73
|
df["date"] = valuation_date.strftime("%Y-%m-%d")
|
|
56
74
|
df["asset_valuation_date"] = pd.to_datetime(df["asset_valuation_date"], dayfirst=True).dt.strftime("%Y-%m-%d")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import re
|
|
3
2
|
|
|
3
|
+
from django.utils.dateparse import parse_date
|
|
4
|
+
|
|
4
5
|
from wbportfolio.models import Product
|
|
5
6
|
|
|
6
7
|
INSTRUMENT_MAP_NAME = {"EDA23_AtonRa Z class": "LU2170995018"}
|
|
@@ -53,25 +54,14 @@ def _get_underlying_instrument(bbg_code, name, currency, instrument_type="equity
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def file_name_parse_isin(file_name):
|
|
56
|
-
dates = re.findall(r"_([0-9]{4}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if len(dates)
|
|
60
|
-
raise ValueError("Not
|
|
61
|
-
|
|
62
|
-
if len(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
valuation_date = datetime.datetime.strptime(dates[0], "%Y%m%d").date()
|
|
68
|
-
try:
|
|
69
|
-
generation_date = datetime.datetime.strptime(dates[1], "%Y-%m-%d").date()
|
|
70
|
-
except ValueError:
|
|
71
|
-
generation_date = datetime.datetime.strptime(dates[1], "%Y%m%d").date()
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
"product": {"isin": identifier[0]},
|
|
75
|
-
"valuation_date": valuation_date,
|
|
76
|
-
"generation_date": generation_date,
|
|
77
|
-
}
|
|
57
|
+
dates = re.findall(r"_([0-9]{4}[-_]?[0-9]{2}[-_]?[0-9]{2})", file_name)
|
|
58
|
+
isin = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
|
|
59
|
+
ticker = re.findall(r"(NX[A-Z]*)_", file_name)
|
|
60
|
+
if len(dates) == 0:
|
|
61
|
+
raise ValueError("Not dates found in the filename")
|
|
62
|
+
res = {"valuation_date": parse_date(dates[0].replace("_", "-"))}
|
|
63
|
+
if len(isin) >= 1:
|
|
64
|
+
res["product"] = {"isin": isin[0]}
|
|
65
|
+
elif len(ticker) == 1:
|
|
66
|
+
res["product"] = {"ticker": ticker[0]}
|
|
67
|
+
return res
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Generated by Django 5.0.12 on 2025-09-18 08:48
|
|
2
|
+
|
|
3
|
+
import django.db.models.expressions
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('wbportfolio', '0089_orderproposal_min_weighting'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='dividendtransaction',
|
|
16
|
+
name='price_fx_portfolio',
|
|
17
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='dividendtransaction',
|
|
21
|
+
name='price_gross_fx_portfolio',
|
|
22
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name='order',
|
|
26
|
+
name='price_fx_portfolio',
|
|
27
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name='order',
|
|
31
|
+
name='price_gross_fx_portfolio',
|
|
32
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
33
|
+
),
|
|
34
|
+
migrations.AddField(
|
|
35
|
+
model_name='trade',
|
|
36
|
+
name='price_fx_portfolio',
|
|
37
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
38
|
+
),
|
|
39
|
+
migrations.AddField(
|
|
40
|
+
model_name='trade',
|
|
41
|
+
name='price_gross_fx_portfolio',
|
|
42
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
43
|
+
),
|
|
44
|
+
]
|
wbportfolio/models/asset.py
CHANGED
|
@@ -409,7 +409,7 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
409
409
|
analytical_objects = AnalyticalAssetPositionManager()
|
|
410
410
|
unannotated_objects = models.Manager()
|
|
411
411
|
|
|
412
|
-
def pre_save(
|
|
412
|
+
def pre_save( # noqa: C901
|
|
413
413
|
self, create_underlying_quote_price_if_missing: bool = False, infer_underlying_quote_price: bool = True
|
|
414
414
|
):
|
|
415
415
|
if not self.asset_valuation_date:
|
wbportfolio/models/builder.py
CHANGED
|
@@ -225,7 +225,6 @@ class AssetPositionBuilder:
|
|
|
225
225
|
|
|
226
226
|
def bulk_create_positions(self, delete_leftovers: bool = False, force_save: bool = False, **kwargs):
|
|
227
227
|
positions = list(self.get_positions(**kwargs))
|
|
228
|
-
|
|
229
228
|
# we need to delete the existing estimated portfolio because otherwise we risk to have existing and not
|
|
230
229
|
# overlapping positions remaining (as they will not be updating by the bulk create). E.g. when someone
|
|
231
230
|
# change completely the trades of a portfolio model and drift it.
|
|
@@ -834,7 +834,7 @@ class LiquidityStressMixin:
|
|
|
834
834
|
|
|
835
835
|
""" The main function for the liquidity stress tests """
|
|
836
836
|
|
|
837
|
-
def liquidity_stress_test(
|
|
837
|
+
def liquidity_stress_test( # noqa: C901
|
|
838
838
|
self,
|
|
839
839
|
report_date: Optional[date] = None,
|
|
840
840
|
weights_date: Optional[date] = None,
|
|
@@ -527,10 +527,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
527
527
|
total_target_weight=self.total_expected_target_weight,
|
|
528
528
|
)
|
|
529
529
|
leftovers_orders = self.orders.all()
|
|
530
|
+
portfolio_value = self.portfolio_total_asset_value
|
|
530
531
|
for underlying_instrument_id, order_dto in service.trades_batch.trades_map.items():
|
|
531
532
|
with suppress(Order.DoesNotExist):
|
|
532
533
|
order = self.orders.get(underlying_instrument_id=underlying_instrument_id)
|
|
533
|
-
order.
|
|
534
|
+
order.set_weighting(round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION), portfolio_value)
|
|
534
535
|
order.save()
|
|
535
536
|
leftovers_orders = leftovers_orders.exclude(id=order.id)
|
|
536
537
|
leftovers_orders.delete()
|
|
@@ -539,11 +540,12 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
539
540
|
def fix_quantization(self):
|
|
540
541
|
if self.orders.exists():
|
|
541
542
|
orders = self.get_orders()
|
|
543
|
+
portfolio_value = self.portfolio_total_asset_value
|
|
542
544
|
t_weight = orders.aggregate(models.Sum("target_weight"))["target_weight__sum"] or Decimal("0.0")
|
|
543
545
|
# we handle quantization error due to the decimal max digits. In that case, we take the biggest order (highest weight) and we remove the quantization error
|
|
544
546
|
if quantize_error := (t_weight - self.total_expected_target_weight):
|
|
545
547
|
biggest_order = orders.exclude(underlying_instrument__is_cash=True).latest("target_weight")
|
|
546
|
-
biggest_order.weighting
|
|
548
|
+
biggest_order.set_weighting(biggest_order.weighting - quantize_error, portfolio_value)
|
|
547
549
|
biggest_order.save()
|
|
548
550
|
|
|
549
551
|
def _get_default_target_portfolio(self, use_desired_target_weight: bool = False, **kwargs) -> PortfolioDTO:
|
|
@@ -594,6 +596,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
594
596
|
orders = service.trades_batch.trades_map.values()
|
|
595
597
|
|
|
596
598
|
objs = []
|
|
599
|
+
portfolio_value = self.portfolio_total_asset_value
|
|
597
600
|
for order_dto in orders:
|
|
598
601
|
instrument = Instrument.objects.get(id=order_dto.underlying_instrument)
|
|
599
602
|
currency_fx_rate = instrument.currency.convert(
|
|
@@ -604,7 +607,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
604
607
|
daily_return = order_dto.daily_return
|
|
605
608
|
try:
|
|
606
609
|
order = self.orders.get(underlying_instrument=instrument)
|
|
607
|
-
order.weighting = weighting
|
|
608
610
|
order.currency_fx_rate = currency_fx_rate
|
|
609
611
|
order.daily_return = daily_return
|
|
610
612
|
except Order.DoesNotExist:
|
|
@@ -616,11 +618,12 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
616
618
|
daily_return=daily_return,
|
|
617
619
|
currency_fx_rate=currency_fx_rate,
|
|
618
620
|
)
|
|
621
|
+
order.pre_save()
|
|
622
|
+
order.set_weighting(weighting, portfolio_value)
|
|
619
623
|
order.desired_target_weight = order_dto.target_weight
|
|
620
624
|
order.order_type = Order.get_type(
|
|
621
625
|
weighting, round(order_dto.previous_weight, 8), round(order_dto.target_weight, 8)
|
|
622
626
|
)
|
|
623
|
-
order.pre_save()
|
|
624
627
|
# # if we cannot automatically find a price, we consider the stock is invalid and we sell it
|
|
625
628
|
# if not order.price and order.weighting > 0:
|
|
626
629
|
# order.price = Decimal("0.0")
|
|
@@ -638,7 +641,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
638
641
|
"price",
|
|
639
642
|
"price_gross",
|
|
640
643
|
"desired_target_weight",
|
|
641
|
-
|
|
644
|
+
"shares",
|
|
642
645
|
],
|
|
643
646
|
unique_fields=["order_proposal", "underlying_instrument"],
|
|
644
647
|
update_conflicts=True,
|
|
@@ -790,7 +793,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
790
793
|
price_fx_portfolio = quote_price * underlying_quote.currency.convert(
|
|
791
794
|
self.trade_date, self.portfolio.currency, exact_lookup=False
|
|
792
795
|
)
|
|
793
|
-
|
|
794
796
|
# If the price is valid, calculate and return the estimated shares
|
|
795
797
|
if price_fx_portfolio:
|
|
796
798
|
return trade_total_value_fx_portfolio / price_fx_portfolio
|
|
@@ -888,6 +890,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
888
890
|
order_warnings = order.submit(
|
|
889
891
|
by=by, description=description, portfolio_total_asset_value=self.portfolio_total_asset_value, **kwargs
|
|
890
892
|
)
|
|
893
|
+
|
|
891
894
|
if order_warnings:
|
|
892
895
|
orders_validation_warnings.extend(order_warnings)
|
|
893
896
|
orders.append(order)
|
|
@@ -71,44 +71,25 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
71
71
|
execution_confirmed = models.BooleanField(default=False, verbose_name="Execution Confirmed")
|
|
72
72
|
execution_comment = models.TextField(default="", blank=True, verbose_name="Execution Comment")
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
warnings = []
|
|
74
|
+
order_with_respect_to = "order_proposal"
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.shares = Decimal("0")
|
|
95
|
-
self.weighting = Decimal("0")
|
|
96
|
-
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
97
|
-
warnings.append(
|
|
98
|
-
f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
|
|
99
|
-
)
|
|
100
|
-
self.shares = Decimal("0")
|
|
101
|
-
self.weighting = Decimal("0")
|
|
102
|
-
if not self.price:
|
|
103
|
-
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
104
|
-
if (
|
|
105
|
-
not self.underlying_instrument.is_cash
|
|
106
|
-
and not self.underlying_instrument.is_cash_equivalent
|
|
107
|
-
and self._target_weight < -1e-8
|
|
108
|
-
): # any value below -1e8 will be considered zero
|
|
109
|
-
warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
|
|
110
|
-
self.desired_target_weight = self._target_weight
|
|
111
|
-
return warnings
|
|
76
|
+
class Meta(OrderedModel.Meta):
|
|
77
|
+
verbose_name = "Order"
|
|
78
|
+
verbose_name_plural = "Orders"
|
|
79
|
+
indexes = [
|
|
80
|
+
models.Index(fields=["order_proposal"]),
|
|
81
|
+
models.Index(fields=["underlying_instrument", "value_date"]),
|
|
82
|
+
models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
|
|
83
|
+
models.Index(fields=["order_proposal", "underlying_instrument"]),
|
|
84
|
+
# models.Index(fields=["date", "underlying_instrument"]),
|
|
85
|
+
]
|
|
86
|
+
constraints = [
|
|
87
|
+
models.UniqueConstraint(
|
|
88
|
+
fields=["order_proposal", "underlying_instrument"],
|
|
89
|
+
name="unique_order",
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
112
93
|
|
|
113
94
|
@property
|
|
114
95
|
def product(self):
|
|
@@ -176,25 +157,9 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
176
157
|
def _target_shares(self) -> Decimal:
|
|
177
158
|
return getattr(self, "target_shares", self._effective_shares + self.shares)
|
|
178
159
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
verbose_name = "Order"
|
|
183
|
-
verbose_name_plural = "Orders"
|
|
184
|
-
indexes = [
|
|
185
|
-
models.Index(fields=["order_proposal"]),
|
|
186
|
-
models.Index(fields=["underlying_instrument", "value_date"]),
|
|
187
|
-
models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
|
|
188
|
-
models.Index(fields=["order_proposal", "underlying_instrument"]),
|
|
189
|
-
# models.Index(fields=["date", "underlying_instrument"]),
|
|
190
|
-
]
|
|
191
|
-
constraints = [
|
|
192
|
-
models.UniqueConstraint(
|
|
193
|
-
fields=["order_proposal", "underlying_instrument"],
|
|
194
|
-
name="unique_order",
|
|
195
|
-
),
|
|
196
|
-
]
|
|
197
|
-
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
160
|
+
def __str__(self):
|
|
161
|
+
ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
|
|
162
|
+
return f"{ticker}{self.weighting}"
|
|
198
163
|
|
|
199
164
|
def pre_save(self):
|
|
200
165
|
self.portfolio = self.order_proposal.portfolio
|
|
@@ -240,15 +205,72 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
240
205
|
else:
|
|
241
206
|
return Order.Type.SELL
|
|
242
207
|
|
|
243
|
-
def set_type(self):
|
|
244
|
-
self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
|
|
245
|
-
|
|
246
208
|
def get_price(self) -> Decimal:
|
|
247
209
|
try:
|
|
248
210
|
return self.underlying_instrument.get_price(self.value_date)
|
|
249
211
|
except ValueError:
|
|
250
212
|
return Decimal("0")
|
|
251
213
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
|
|
214
|
+
def set_type(self):
|
|
215
|
+
self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
|
|
216
|
+
|
|
217
|
+
def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
|
|
218
|
+
self.weighting = weighting
|
|
219
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
220
|
+
if price_fx_portfolio and portfolio_value:
|
|
221
|
+
total_value = self.weighting * portfolio_value
|
|
222
|
+
self.shares = total_value / price_fx_portfolio
|
|
223
|
+
else:
|
|
224
|
+
self.shares = Decimal("0")
|
|
225
|
+
|
|
226
|
+
def set_shares(self, shares: Decimal, portfolio_value: Decimal):
|
|
227
|
+
if portfolio_value:
|
|
228
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
229
|
+
self.shares = shares
|
|
230
|
+
total_value = shares * price_fx_portfolio
|
|
231
|
+
self.weighting = total_value / portfolio_value
|
|
232
|
+
else:
|
|
233
|
+
self.weighting = self.shares = Decimal("0")
|
|
234
|
+
|
|
235
|
+
def set_total_value_fx_portfolio(self, total_value_fx_portfolio: Decimal, portfolio_value: Decimal):
|
|
236
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
237
|
+
if price_fx_portfolio and portfolio_value:
|
|
238
|
+
self.shares = total_value_fx_portfolio / price_fx_portfolio
|
|
239
|
+
self.weighting = total_value_fx_portfolio / portfolio_value
|
|
240
|
+
else:
|
|
241
|
+
self.weighting = self.shares = Decimal("0")
|
|
242
|
+
|
|
243
|
+
def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
|
|
244
|
+
warnings = []
|
|
245
|
+
|
|
246
|
+
# if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
|
|
247
|
+
if self.order_proposal and not self.portfolio.only_weighting:
|
|
248
|
+
shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
|
|
249
|
+
if shares != self.shares:
|
|
250
|
+
warnings.append(
|
|
251
|
+
f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
|
|
252
|
+
)
|
|
253
|
+
shares = round(shares) # ensure fractional shares are converted into integer
|
|
254
|
+
# we need to recompute the delta weight has we changed the number of shares
|
|
255
|
+
if shares != self.shares:
|
|
256
|
+
self.set_shares(shares, portfolio_total_asset_value)
|
|
257
|
+
if abs(self.weighting) < self.order_proposal.min_weighting:
|
|
258
|
+
warnings.append(
|
|
259
|
+
f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
|
|
260
|
+
)
|
|
261
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
262
|
+
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
263
|
+
warnings.append(
|
|
264
|
+
f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
|
|
265
|
+
)
|
|
266
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
267
|
+
if not self.price:
|
|
268
|
+
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
269
|
+
if (
|
|
270
|
+
not self.underlying_instrument.is_cash
|
|
271
|
+
and not self.underlying_instrument.is_cash_equivalent
|
|
272
|
+
and self._target_weight < -1e-8
|
|
273
|
+
): # any value below -1e8 will be considered zero
|
|
274
|
+
warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
|
|
275
|
+
self.desired_target_weight = self._target_weight
|
|
276
|
+
return warnings
|
|
@@ -194,6 +194,7 @@ class Trade(TransactionMixin, ImportMixin, models.Model):
|
|
|
194
194
|
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
195
195
|
|
|
196
196
|
def save(self, *args, **kwargs):
|
|
197
|
+
self.pre_save()
|
|
197
198
|
if abs(self.weighting) < 10e-6:
|
|
198
199
|
self.weighting = Decimal("0")
|
|
199
200
|
if not self.price:
|
|
@@ -71,6 +71,22 @@ class TransactionMixin(models.Model):
|
|
|
71
71
|
),
|
|
72
72
|
db_persist=True,
|
|
73
73
|
)
|
|
74
|
+
price_fx_portfolio = models.GeneratedField(
|
|
75
|
+
expression=models.F("currency_fx_rate") * models.F("price"),
|
|
76
|
+
output_field=models.DecimalField(
|
|
77
|
+
max_digits=20,
|
|
78
|
+
decimal_places=4,
|
|
79
|
+
),
|
|
80
|
+
db_persist=True,
|
|
81
|
+
)
|
|
82
|
+
price_gross_fx_portfolio = models.GeneratedField(
|
|
83
|
+
expression=models.F("currency_fx_rate") * models.F("price_gross"),
|
|
84
|
+
output_field=models.DecimalField(
|
|
85
|
+
max_digits=20,
|
|
86
|
+
decimal_places=4,
|
|
87
|
+
),
|
|
88
|
+
db_persist=True,
|
|
89
|
+
)
|
|
74
90
|
total_value_fx_portfolio = models.GeneratedField(
|
|
75
91
|
expression=models.F("currency_fx_rate") * models.F("price") * models.F("shares"),
|
|
76
92
|
output_field=models.DecimalField(
|
|
@@ -100,14 +116,10 @@ class TransactionMixin(models.Model):
|
|
|
100
116
|
self.price_gross = self.price
|
|
101
117
|
elif self.price_gross is not None and self.price is None:
|
|
102
118
|
self.price = self.price_gross
|
|
103
|
-
|
|
104
|
-
def save(self, *args, **kwargs):
|
|
105
|
-
self.pre_save()
|
|
106
119
|
if self.currency_fx_rate is None:
|
|
107
120
|
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
108
121
|
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
109
122
|
)
|
|
110
|
-
super().save(*args, **kwargs)
|
|
111
123
|
|
|
112
124
|
class Meta:
|
|
113
125
|
abstract = True
|
|
@@ -180,7 +180,7 @@ class RuleBackend(
|
|
|
180
180
|
breached_value = f'<span style="color:green">{breached_value}</span>'
|
|
181
181
|
yield backend.IncidentResult(
|
|
182
182
|
breached_object=obj,
|
|
183
|
-
breached_object_repr=obj_repr,
|
|
183
|
+
breached_object_repr=str(obj_repr),
|
|
184
184
|
breached_value=breached_value,
|
|
185
185
|
report_details=self.report_details,
|
|
186
186
|
severity=severity,
|
|
@@ -218,7 +218,7 @@ class RuleBackend(
|
|
|
218
218
|
obj = Instrument.objects.get(id=pivot_object_id)
|
|
219
219
|
return obj, str(obj)
|
|
220
220
|
case self.GroupbyChoices.ASSET_TYPE:
|
|
221
|
-
return None, InstrumentType.objects.get(id=pivot_object_id)
|
|
221
|
+
return None, InstrumentType.objects.get(id=pivot_object_id).name
|
|
222
222
|
case self.GroupbyChoices.CASH:
|
|
223
223
|
return None, "Cash"
|
|
224
224
|
case self.GroupbyChoices.COUNTRY:
|
|
@@ -138,5 +138,5 @@ class TestExposurePortfolioRuleModel(PortfolioTestMixin):
|
|
|
138
138
|
incidents = list(exposure_portfolio_backend.check_rule())
|
|
139
139
|
assert len(incidents) == 1
|
|
140
140
|
incident = incidents[0]
|
|
141
|
-
assert incident.breached_object_repr == i1.instrument_type
|
|
141
|
+
assert incident.breached_object_repr == i1.instrument_type.name
|
|
142
142
|
assert incident.breached_value == '<span style="color:green">+5.00%</span>'
|
|
@@ -44,29 +44,26 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
44
44
|
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
45
45
|
underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
effective_weight = wb_serializers.DecimalField(
|
|
48
|
+
read_only=True,
|
|
48
49
|
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
49
50
|
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
50
|
-
required=False,
|
|
51
51
|
default=0,
|
|
52
52
|
)
|
|
53
|
-
|
|
54
|
-
read_only=True,
|
|
53
|
+
target_weight = wb_serializers.DecimalField(
|
|
55
54
|
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
56
55
|
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
57
|
-
|
|
56
|
+
required=False,
|
|
58
57
|
)
|
|
59
58
|
|
|
60
59
|
effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
61
|
-
target_shares = wb_serializers.DecimalField(
|
|
60
|
+
target_shares = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=6)
|
|
62
61
|
|
|
63
|
-
total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
|
|
64
62
|
effective_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
65
63
|
read_only=True, max_digits=16, decimal_places=2, default=0
|
|
66
64
|
)
|
|
67
|
-
target_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
68
|
-
|
|
69
|
-
)
|
|
65
|
+
target_total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
66
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
70
67
|
|
|
71
68
|
portfolio_currency = wb_serializers.CharField(read_only=True)
|
|
72
69
|
has_warnings = wb_serializers.BooleanField(read_only=True)
|
|
@@ -81,17 +78,57 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
81
78
|
}
|
|
82
79
|
)
|
|
83
80
|
effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
data["
|
|
90
|
-
|
|
91
|
-
data
|
|
92
|
-
|
|
81
|
+
effective_shares = self.instance._effective_shares if self.instance else Decimal(0.0)
|
|
82
|
+
portfolio_value = (
|
|
83
|
+
self.context["view"].order_proposal.portfolio_total_asset_value if "view" in self.context else Decimal(0.0)
|
|
84
|
+
)
|
|
85
|
+
if (total_value_fx_portfolio := data.pop("total_value_fx_portfolio", None)) is not None and portfolio_value:
|
|
86
|
+
data["weighting"] = total_value_fx_portfolio / portfolio_value
|
|
87
|
+
if (
|
|
88
|
+
target_total_value_fx_portfolio := data.pop("target_total_value_fx_portfolio", None)
|
|
89
|
+
) is not None and portfolio_value:
|
|
90
|
+
data["target_weight"] = target_total_value_fx_portfolio / portfolio_value
|
|
91
|
+
|
|
92
|
+
if data.get("weighting") or data.get("target_weight"):
|
|
93
|
+
weighting = data.pop("weighting", None)
|
|
94
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
95
|
+
weighting = target_weight - effective_weight
|
|
96
|
+
data["desired_target_weight"] = target_weight
|
|
97
|
+
if weighting is not None:
|
|
98
|
+
data["weighting"] = weighting
|
|
99
|
+
data.pop("shares", None)
|
|
100
|
+
data.pop("target_shares", None)
|
|
101
|
+
|
|
102
|
+
if data.get("shares") or data.get("target_shares"):
|
|
103
|
+
shares = data.pop("shares", None)
|
|
104
|
+
if (target_shares := data.pop("target_shares", None)) is not None:
|
|
105
|
+
shares = target_shares - effective_shares
|
|
106
|
+
if shares is not None:
|
|
107
|
+
data["shares"] = shares
|
|
93
108
|
return super().validate(data)
|
|
94
109
|
|
|
110
|
+
def update(self, instance, validated_data):
|
|
111
|
+
weighting = validated_data.pop("weighting", None)
|
|
112
|
+
shares = validated_data.pop("shares", None)
|
|
113
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
114
|
+
if weighting is not None:
|
|
115
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
116
|
+
if shares is not None:
|
|
117
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
118
|
+
return super().update(instance, validated_data)
|
|
119
|
+
|
|
120
|
+
def create(self, validated_data):
|
|
121
|
+
weighting = validated_data.pop("weighting", None)
|
|
122
|
+
shares = validated_data.pop("shares", None)
|
|
123
|
+
instance = super().create(validated_data)
|
|
124
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
125
|
+
if weighting is not None:
|
|
126
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
127
|
+
if shares is not None:
|
|
128
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
129
|
+
instance.save()
|
|
130
|
+
return instance
|
|
131
|
+
|
|
95
132
|
class Meta:
|
|
96
133
|
model = Order
|
|
97
134
|
percent_fields = ["effective_weight", "target_weight", "weighting"]
|
|
@@ -108,12 +145,8 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
108
145
|
}
|
|
109
146
|
read_only_fields = (
|
|
110
147
|
"order_type",
|
|
111
|
-
"shares",
|
|
112
148
|
"effective_shares",
|
|
113
|
-
"target_shares",
|
|
114
|
-
"total_value_fx_portfolio",
|
|
115
149
|
"effective_total_value_fx_portfolio",
|
|
116
|
-
"target_total_value_fx_portfolio",
|
|
117
150
|
"has_warnings",
|
|
118
151
|
)
|
|
119
152
|
fields = (
|
|
@@ -500,17 +500,25 @@ class TestOrderProposal:
|
|
|
500
500
|
instrument.exchange.save()
|
|
501
501
|
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
502
502
|
|
|
503
|
-
|
|
503
|
+
@patch.object(Portfolio, "get_total_asset_value")
|
|
504
|
+
def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
|
|
505
|
+
initial_shares = Decimal("70")
|
|
506
|
+
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
507
|
+
net_value = round(price.net_value, 4)
|
|
508
|
+
portfolio_value = initial_shares * net_value
|
|
509
|
+
mock_fct.return_value = portfolio_value
|
|
510
|
+
|
|
504
511
|
order_proposal.portfolio.only_weighting = False
|
|
505
512
|
order_proposal.portfolio.save()
|
|
506
|
-
instrument =
|
|
513
|
+
instrument = price.instrument
|
|
507
514
|
instrument.round_lot_size = 100
|
|
508
515
|
instrument.save()
|
|
509
516
|
trade = order_factory.create(
|
|
510
|
-
|
|
511
|
-
shares=70,
|
|
517
|
+
shares=initial_shares,
|
|
512
518
|
order_proposal=order_proposal,
|
|
513
519
|
weighting=Decimal("1.0"),
|
|
520
|
+
underlying_instrument=price.instrument,
|
|
521
|
+
price=net_value,
|
|
514
522
|
)
|
|
515
523
|
warnings = order_proposal.submit()
|
|
516
524
|
order_proposal.save()
|
|
@@ -520,18 +528,31 @@ class TestOrderProposal:
|
|
|
520
528
|
trade.refresh_from_db()
|
|
521
529
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
522
530
|
|
|
523
|
-
|
|
531
|
+
@patch.object(Portfolio, "get_total_asset_value")
|
|
532
|
+
def test_submit_round_fractional_shares(self, mock_fct, instrument_price_factory, order_proposal, order_factory):
|
|
533
|
+
initial_shares = Decimal("5.6")
|
|
534
|
+
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
535
|
+
net_value = round(price.net_value, 4)
|
|
536
|
+
portfolio_value = initial_shares * net_value
|
|
537
|
+
mock_fct.return_value = portfolio_value
|
|
538
|
+
|
|
524
539
|
order_proposal.portfolio.only_weighting = False
|
|
525
540
|
order_proposal.portfolio.save()
|
|
526
541
|
trade = order_factory.create(
|
|
527
|
-
shares=5.6,
|
|
542
|
+
shares=Decimal("5.6"),
|
|
528
543
|
order_proposal=order_proposal,
|
|
529
544
|
weighting=Decimal("1.0"),
|
|
545
|
+
underlying_instrument=price.instrument,
|
|
546
|
+
price=net_value,
|
|
530
547
|
)
|
|
531
548
|
order_proposal.submit()
|
|
532
549
|
order_proposal.save()
|
|
533
550
|
trade.refresh_from_db()
|
|
534
551
|
assert trade.shares == 6 # we expect the fractional share to be rounded
|
|
552
|
+
assert trade.weighting == round((trade.shares * net_value) / portfolio_value, 8)
|
|
553
|
+
assert trade.weighting == round(
|
|
554
|
+
Decimal("1") + ((Decimal("6") - initial_shares) * net_value) / portfolio_value, 8
|
|
555
|
+
) # we expect the weighting to be updated accrodingly
|
|
535
556
|
|
|
536
557
|
def test_ex_post(
|
|
537
558
|
self, instrument_factory, asset_position_factory, instrument_price_factory, order_proposal_factory, portfolio
|
|
@@ -719,7 +740,6 @@ class TestOrderProposal:
|
|
|
719
740
|
order_proposal.approve()
|
|
720
741
|
order_proposal.apply()
|
|
721
742
|
order_proposal.save()
|
|
722
|
-
|
|
723
743
|
assert order_proposal.portfolio.assets.get(
|
|
724
744
|
date=order_proposal.trade_date, underlying_quote=o1.underlying_instrument
|
|
725
745
|
).weighting == Decimal("0.8")
|
|
@@ -160,6 +160,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
160
160
|
return df.reset_index(names="id")
|
|
161
161
|
|
|
162
162
|
def manipulate_dataframe(self, df):
|
|
163
|
+
df["id"] = df["id"].fillna(-1)
|
|
163
164
|
if self.group_by == AssetPositionGroupBy.INDUSTRY:
|
|
164
165
|
if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
|
|
165
166
|
df["equity"] = ""
|
|
@@ -182,6 +183,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
|
|
|
182
183
|
df["label"] = df["id"].map(
|
|
183
184
|
dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
|
|
184
185
|
)
|
|
186
|
+
df.loc[df["id"] == -1, "label"] = "N/A"
|
|
185
187
|
df.sort_values(by="weighting", ascending=False, inplace=True)
|
|
186
188
|
return df
|
|
187
189
|
|
|
@@ -173,9 +173,27 @@ class OrderOrderProposalModelViewSet(
|
|
|
173
173
|
def get_serializer_class(self):
|
|
174
174
|
if self.order_proposal.status != OrderProposal.Status.DRAFT:
|
|
175
175
|
return ReadOnlyOrderOrderProposalModelSerializer
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
if not self.new_mode and "pk" not in self.kwargs:
|
|
177
|
+
serializer_base_class = OrderOrderProposalListModelSerializer
|
|
178
|
+
else:
|
|
179
|
+
serializer_base_class = OrderOrderProposalModelSerializer
|
|
180
|
+
if not self.order_proposal.portfolio_total_asset_value:
|
|
181
|
+
|
|
182
|
+
class OnlyWeightSerializerClass(serializer_base_class):
|
|
183
|
+
class Meta(serializer_base_class.Meta):
|
|
184
|
+
read_only_fields = (
|
|
185
|
+
"order_type",
|
|
186
|
+
"effective_shares",
|
|
187
|
+
"effective_total_value_fx_portfolio",
|
|
188
|
+
"has_warnings",
|
|
189
|
+
"shares",
|
|
190
|
+
"target_shares",
|
|
191
|
+
"total_value_fx_portfolio",
|
|
192
|
+
"target_total_value_fx_portfolio",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return OnlyWeightSerializerClass
|
|
196
|
+
return serializer_base_class
|
|
179
197
|
|
|
180
198
|
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
181
199
|
if self.orders.exists() and self.order_proposal.status in [
|
|
@@ -214,7 +232,7 @@ class OrderOrderProposalModelViewSet(
|
|
|
214
232
|
default=F("underlying_instrument__instrument_type__short_name"),
|
|
215
233
|
),
|
|
216
234
|
underlying_instrument_exchange=F("underlying_instrument__exchange__name"),
|
|
217
|
-
effective_total_value_fx_portfolio=F("
|
|
235
|
+
effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
|
|
218
236
|
target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
|
|
219
237
|
portfolio_currency=F("portfolio__currency__symbol"),
|
|
220
238
|
security=F("underlying_instrument__parent"),
|
|
@@ -80,13 +80,13 @@ wbportfolio/factories/orders/__init__.py,sha256=QG7on_F9GiuJgvGbM0BNDVdfGwwBz_WG
|
|
|
80
80
|
wbportfolio/factories/orders/order_proposals.py,sha256=3QGG6eI2sWYGJenn17gz37A2PJgjZLlPqAsqci1lFXI,702
|
|
81
81
|
wbportfolio/factories/orders/orders.py,sha256=tTw2U-xvUsFpG4zad3KCHZPQx1FVg-zPjwEN73XEOjg,907
|
|
82
82
|
wbportfolio/filters/__init__.py,sha256=m44CdyYMtku8rMGyiX8KSmMdvyqaElKW4UMkEqOC1VM,1374
|
|
83
|
-
wbportfolio/filters/assets.py,sha256=
|
|
83
|
+
wbportfolio/filters/assets.py,sha256=1nf8rswmwYPEI_pbD107f4SkyJjKLJMqrUEjp7oHFtQ,18793
|
|
84
84
|
wbportfolio/filters/assets_and_net_new_money_progression.py,sha256=IYl_qMFO9QKhXhCTtz2JrOjlAKdEkykNlu4DO_H7HkQ,1403
|
|
85
85
|
wbportfolio/filters/custodians.py,sha256=pStQgPQPhPpnt57_V7BuXbFXmRiZBEAiEeMFuQmt2NE,287
|
|
86
86
|
wbportfolio/filters/esg.py,sha256=ZEUQh3IQxLSshVrgtPqHC_ToirrllkCXR_KzUlCUtkA,688
|
|
87
87
|
wbportfolio/filters/performances.py,sha256=xJC7fe8XNPz159cewMD8Es8u89vzgpZT4D2Gm1YToM8,7340
|
|
88
88
|
wbportfolio/filters/portfolios.py,sha256=rQL77Cs1YVR8I_kSZwYppJ9yPj8ebWrDQHs_sOI_Ndk,2247
|
|
89
|
-
wbportfolio/filters/positions.py,sha256=
|
|
89
|
+
wbportfolio/filters/positions.py,sha256=JymXAxo3t_t-5g0yzIHNz9s0MmuozEuK6itwpg7hsec,8475
|
|
90
90
|
wbportfolio/filters/products.py,sha256=jOoRKDm_2n2a-Y1dHWl1zjdrwy4Cek57Qa3Q10p_zzY,6492
|
|
91
91
|
wbportfolio/filters/roles.py,sha256=jb8WLzZ_e03x8XkAWuYAdPgMyre3h-rtgYXKJ3dqkhw,920
|
|
92
92
|
wbportfolio/filters/signals.py,sha256=XZ1d50yas47Xy57ZfmvCVjMSKojaoqrc5FHJBcVadtk,3144
|
|
@@ -95,9 +95,9 @@ wbportfolio/filters/orders/order_proposals.py,sha256=V_lICJBEG1aOVaYiaIi1KcIn8uE
|
|
|
95
95
|
wbportfolio/filters/orders/orders.py,sha256=eRVb9w7zHq6bPnHOd3gC1k6OJYSpQ6PeXxu2dxJe1A0,289
|
|
96
96
|
wbportfolio/filters/transactions/__init__.py,sha256=TXiHB7_ItZPVcXCCGejpLBRD4R7rtt3Qrq59z8D5ChA,582
|
|
97
97
|
wbportfolio/filters/transactions/claim.py,sha256=eSykgNzcytMWWTmlGlaqBuh98vfOKoTLvA3cglc_jZ0,17951
|
|
98
|
-
wbportfolio/filters/transactions/fees.py,sha256=
|
|
98
|
+
wbportfolio/filters/transactions/fees.py,sha256=osm7vOqoKFJi3KaylIr5axHmmhDzd4rKSvU8zs-nVRk,1608
|
|
99
99
|
wbportfolio/filters/transactions/mixins.py,sha256=TEV3MUsiQTeu4NdFYHMIIMonmC7CdFF80JTpWYIvfRQ,550
|
|
100
|
-
wbportfolio/filters/transactions/trades.py,sha256=
|
|
100
|
+
wbportfolio/filters/transactions/trades.py,sha256=7k_r1m1zDiQWCx3g6eUeWjPV18bbU4EV2Jeic9FjnyA,9991
|
|
101
101
|
wbportfolio/filters/transactions/utils.py,sha256=hU133SdUDuD30wy9QLvCCJbGsiZzxvdOp8xtnGbUzaU,1510
|
|
102
102
|
wbportfolio/fixtures/product_factsheets.yaml,sha256=z5o-viDbgwWkssDJXZYayasFgw1nJ0uiOiYrJNDkRtg,5455
|
|
103
103
|
wbportfolio/fixtures/wbportfolio.yaml.gz,sha256=902nxQZM6VcVcc0wI9AYaSedcJIfsK5letLF31j1Jdg,1479453
|
|
@@ -124,7 +124,7 @@ wbportfolio/import_export/handlers/fees.py,sha256=RoOaBwnIMzCx5q7qSzCQbS3L_eOa9Y
|
|
|
124
124
|
wbportfolio/import_export/handlers/orders.py,sha256=GU3_tIy-tAw9aU-ifsnmMZPBB9sqfkFC_S1d9VziTwg,3136
|
|
125
125
|
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
|
|
126
126
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
127
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
127
|
+
wbportfolio/import_export/handlers/trade.py,sha256=d0FSyUQLX8JKBl2fA9BF-AiXCO6TNUWE-oHL5NJavQE,11201
|
|
128
128
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
129
129
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=YdqvoRNo4CbMnMpeYA5DSHSj9fc7rZA4_q9Zeojg344,1393
|
|
130
130
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -146,10 +146,10 @@ wbportfolio/import_export/parsers/natixis/d1_fees.py,sha256=RmzwlNqLSlGDp7JMTwHu
|
|
|
146
146
|
wbportfolio/import_export/parsers/natixis/d1_trade.py,sha256=JsAVgRH2iBo096Spz8ThJOGPpOCPyYrJ9GJ1XnzjBJQ,2092
|
|
147
147
|
wbportfolio/import_export/parsers/natixis/d1_valuation.py,sha256=M12am0ZenUJxkvEGp6tM1HfeukqYS3b6tfbyfC0XwvY,1216
|
|
148
148
|
wbportfolio/import_export/parsers/natixis/dividend.py,sha256=qx8DXu6NXMy1M_iunA4ITM845_6iNd1aoVCdoAKDNsY,1964
|
|
149
|
-
wbportfolio/import_export/parsers/natixis/equity.py,sha256=
|
|
149
|
+
wbportfolio/import_export/parsers/natixis/equity.py,sha256=v5ai-9DyG2fayOzIR21cbwASStXtCobN0Devb1snCLo,3292
|
|
150
150
|
wbportfolio/import_export/parsers/natixis/fees.py,sha256=oIC9moGm4s-6525q4_Syt4Yexj3SDgB5kYSfcsdzLcY,1666
|
|
151
151
|
wbportfolio/import_export/parsers/natixis/trade.py,sha256=BJum1W0fYe9emHU7RgL-AjHkiclrckQYkG8VkyCnfvM,2579
|
|
152
|
-
wbportfolio/import_export/parsers/natixis/utils.py,sha256=
|
|
152
|
+
wbportfolio/import_export/parsers/natixis/utils.py,sha256=ImBHHLkC621ZrUDWI2p-F3AGElprT1FVb7xkPwKwvx0,2490
|
|
153
153
|
wbportfolio/import_export/parsers/natixis/valuation.py,sha256=qk5eDz-b9bIttDGnZxQZnm9pDIalb-LCGxmeTyMKRmQ,1523
|
|
154
154
|
wbportfolio/import_export/parsers/refinitiv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
155
155
|
wbportfolio/import_export/parsers/refinitiv/adjustment.py,sha256=64AQoLOZQWn5HCLpflJ4OgQB3gCB3w_6Y4YLhcmuClg,819
|
|
@@ -270,11 +270,12 @@ wbportfolio/migrations/0086_orderproposal_total_cash_weight.py,sha256=IE65OYYBOS
|
|
|
270
270
|
wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py,sha256=4OHTjdXNwJ-My8EgUGYWM_vRBccjYgghjN4fLV0n1yQ,4564
|
|
271
271
|
wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py,sha256=6Q41GSbWg_L8r6FUcQ9MrdpRazOdUlc7tD8_bmg8a3Y,521
|
|
272
272
|
wbportfolio/migrations/0089_orderproposal_min_weighting.py,sha256=DgJdIRka1eZSrEZqkUBHiVPpCfOvChWPrWdx-ZExO6Q,3906
|
|
273
|
+
wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py,sha256=2HOOaTe9dV5RKKLFNXJwP6u3NMBFZwv7ctN_EJ6Z6A8,2447
|
|
273
274
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
274
275
|
wbportfolio/models/__init__.py,sha256=qU4e7HKyh8NL_0Mg92PcbHTewCv7Ya2gei1DMGe1LWE,980
|
|
275
276
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
276
|
-
wbportfolio/models/asset.py,sha256=
|
|
277
|
-
wbportfolio/models/builder.py,sha256=
|
|
277
|
+
wbportfolio/models/asset.py,sha256=g4HDZKCXVXNY3MKfgbyjFuGTARJqTnBMVgT2xb0H7V8,39210
|
|
278
|
+
wbportfolio/models/builder.py,sha256=lVYW0iW8-RFL6OEpn1bC4VPUY89dSU1YNuFwC4yGIAo,14027
|
|
278
279
|
wbportfolio/models/custodians.py,sha256=QhSC3mfd6rSPp8wizabLbKmFDrrskZTSkxNdBJMdtCk,3815
|
|
279
280
|
wbportfolio/models/exceptions.py,sha256=EZnqSr5PxikiS4oDknRakEmlninJh_c-tOHRYv3IMjE,57
|
|
280
281
|
wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
|
|
@@ -296,10 +297,10 @@ wbportfolio/models/graphs/utils.py,sha256=4QlUkRaTToXwkHBWEPj_eWABntpgl-8gkTqEEf
|
|
|
296
297
|
wbportfolio/models/llm/wbcrm/analyze_relationship.py,sha256=etKdmt9m6Ra5u7EA9t98RVSqFKpbFd4iwBVaH9T26M8,2320
|
|
297
298
|
wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
298
299
|
wbportfolio/models/mixins/instruments.py,sha256=SuMPquQ93D4pZMK-4hQbJtV58_NOyf3wVOctQq7LNXQ,7054
|
|
299
|
-
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=
|
|
300
|
+
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=I_pgJ3QVFBtq0SeljJerhtlrZESRDNAe4On6QfMHvXc,58834
|
|
300
301
|
wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
|
|
301
|
-
wbportfolio/models/orders/order_proposals.py,sha256=
|
|
302
|
-
wbportfolio/models/orders/orders.py,sha256
|
|
302
|
+
wbportfolio/models/orders/order_proposals.py,sha256=fzj6UwikU5IIs04s4wjPD9S71I-CzVHPgY8ZgcC7EpE,58463
|
|
303
|
+
wbportfolio/models/orders/orders.py,sha256=-bIle6Ftt_eRfP5R_J2Y4LEh9HqOUF0sbLUxQftd684,11588
|
|
303
304
|
wbportfolio/models/orders/routing.py,sha256=OpeP-rfMm1UN7a3vbNxBEQxqtGfE57AeGWnbTZa1fRQ,2223
|
|
304
305
|
wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
|
|
305
306
|
wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
|
|
@@ -307,10 +308,10 @@ wbportfolio/models/reconciliations/account_reconciliations.py,sha256=rofSxetFfEJ
|
|
|
307
308
|
wbportfolio/models/reconciliations/reconciliations.py,sha256=kF-BNhUoT4TCn1RIgPSkdEk1iX4NQeZlGGFd_ZulAZU,686
|
|
308
309
|
wbportfolio/models/transactions/__init__.py,sha256=NrnVHxcpi0Kc6SkTiSlWn4JMWoelvkE9bNouClcvqGA,133
|
|
309
310
|
wbportfolio/models/transactions/claim.py,sha256=A_1OBULlkOWCNr-d-g9ySSTeqCdao4_7BDwChBpB7io,25900
|
|
310
|
-
wbportfolio/models/transactions/dividends.py,sha256=
|
|
311
|
+
wbportfolio/models/transactions/dividends.py,sha256=KNqOMRv6KB_Du5dyap3Q7f9AZ9dMAxpaY9I7-YuNNjs,1914
|
|
311
312
|
wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
|
|
312
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
313
|
-
wbportfolio/models/transactions/transactions.py,sha256=
|
|
313
|
+
wbportfolio/models/transactions/trades.py,sha256=fzt52pD7GnvIhVGX8NuArFVzxKEQfPc-Qyow-pAcVvI,16364
|
|
314
|
+
wbportfolio/models/transactions/transactions.py,sha256=X7vT0t6UNHFKVU_vCZsA_YQJuOpYisF1vVEeKaZD5CA,4245
|
|
314
315
|
wbportfolio/order_routing/__init__.py,sha256=kYloUGytPBNB8KVy5ysm9PlEEU_sGPk5c3ch46dXeJo,604
|
|
315
316
|
wbportfolio/order_routing/adapters/__init__.py,sha256=YiM5FFvLucDTBgIn3mCBjKNSRU7K2pokHdbWBoaZA94,1549
|
|
316
317
|
wbportfolio/order_routing/adapters/ubs.py,sha256=_Z9-j1a6_F08VlNIjLLn5XmhoKSZQTTH14lGeTgbu9A,6328
|
|
@@ -336,7 +337,7 @@ wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
336
337
|
wbportfolio/risk_management/backends/__init__.py,sha256=4N3GYcIb0XgZUQWEHxCejOCRabUtLM6ha9UdOWJKUfI,368
|
|
337
338
|
wbportfolio/risk_management/backends/accounts.py,sha256=VMHwhzeFV2bzobb1RlEJadTVix1lkbbfdpo-zG3YN5c,7988
|
|
338
339
|
wbportfolio/risk_management/backends/controversy_portfolio.py,sha256=aoRt29QGFNWPf_7yr0Dpjv2AwsA0SJpZdNf8NCfe7JY,2843
|
|
339
|
-
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=
|
|
340
|
+
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=yqWodIYNkjelTModuLCV1kRXIRiYa55zKrNzlQ9Irso,10770
|
|
340
341
|
wbportfolio/risk_management/backends/instrument_list_portfolio.py,sha256=PS-OtT0ReFnZ-bsOCtAQhPjhE0Nc5AyEqy4gLya3fhk,4177
|
|
341
342
|
wbportfolio/risk_management/backends/liquidity_risk.py,sha256=OnuUypmN86RDnch1JOb8UJo5j1z1_X7sjazqAo7cbwM,3694
|
|
342
343
|
wbportfolio/risk_management/backends/liquidity_stress_instrument.py,sha256=oitzsaZu-HhYn9Avku3322GtDmf6QGsfyRzGPGZoM1Y,3612
|
|
@@ -349,7 +350,7 @@ wbportfolio/risk_management/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
|
|
|
349
350
|
wbportfolio/risk_management/tests/conftest.py,sha256=ChVocx8SDmh13ixkRmnKe_MMudQAX0495o7nG4NDlsg,440
|
|
350
351
|
wbportfolio/risk_management/tests/test_accounts.py,sha256=q9Z3hDw0Yw86VealdmUfiJBkzRcqDwfybteGxS0hAJA,3990
|
|
351
352
|
wbportfolio/risk_management/tests/test_controversy_portfolio.py,sha256=TrvDQ8JEXYFMl7z6MsPAAXV5d8LILO1RYRjMtF9YVTU,1366
|
|
352
|
-
wbportfolio/risk_management/tests/test_exposure_portfolio.py,sha256=
|
|
353
|
+
wbportfolio/risk_management/tests/test_exposure_portfolio.py,sha256=nlEm63OovBKxw0NgE93mY4lvN6i79iS1bHEYeejBkc4,5541
|
|
353
354
|
wbportfolio/risk_management/tests/test_instrument_list_portfolio.py,sha256=1pAsuMhxOWZHxQnVlTnRuzq8qIe6_86EYbkTnPlWRjE,2763
|
|
354
355
|
wbportfolio/risk_management/tests/test_liquidity_risk.py,sha256=UtzBrW7LeY0Z0ARX9pg4CUjgdw8u5Hfrdkddro5EEZ8,1713
|
|
355
356
|
wbportfolio/risk_management/tests/test_product_integrity.py,sha256=wqJ4b5DO2BrEVQF4eVX2B9AvfrRMAyC_lppGrgKc_ug,1959
|
|
@@ -375,7 +376,7 @@ wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzz
|
|
|
375
376
|
wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
|
|
376
377
|
wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
|
|
377
378
|
wbportfolio/serializers/orders/order_proposals.py,sha256=Jxea2-Ze8Id5URv4UV-vTfCQGt11tjR27vRRfCs0gXU,4791
|
|
378
|
-
wbportfolio/serializers/orders/orders.py,sha256=
|
|
379
|
+
wbportfolio/serializers/orders/orders.py,sha256=OfE2ETFu0MvxDWh7o6_4nFunqmKQ-edNT0bNxyF-No4,9473
|
|
379
380
|
wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
|
|
380
381
|
wbportfolio/serializers/transactions/claim.py,sha256=mEt67F2v8HC6roemDT3S0dD0cZIVl1U9sASbLW3Vpyo,11611
|
|
381
382
|
wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
|
|
@@ -419,7 +420,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
|
|
|
419
420
|
wbportfolio/tests/models/test_splits.py,sha256=0PvW6WunBByDYRhtFV2LbK-DV92q-1s3kn45NvBL5Es,9500
|
|
420
421
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
421
422
|
wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
422
|
-
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=
|
|
423
|
+
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=Qv8lk2Fehtpgg79JyEZHRGrp02C4uayeRf4EuWeGNSI,36510
|
|
423
424
|
wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
424
425
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
425
426
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
@@ -458,7 +459,7 @@ wbportfolio/viewsets/reconciliations.py,sha256=tRS7UBDtMn-07uA9QqYvnCtp_mZGVeULF
|
|
|
458
459
|
wbportfolio/viewsets/registers.py,sha256=eOuEhW2McHsOapCKue7M4eazrJnVpCBVxImpFccq0z0,2401
|
|
459
460
|
wbportfolio/viewsets/roles.py,sha256=vnzS1mlJMZS3GObrss6SfXOr3KRb-OHB1s4Msvtn7xQ,1556
|
|
460
461
|
wbportfolio/viewsets/charts/__init__.py,sha256=zE3bxS0V-nM_yZ610M7qsaToNOJXvTjwFLhH1it3h4M,169
|
|
461
|
-
wbportfolio/viewsets/charts/assets.py,sha256=
|
|
462
|
+
wbportfolio/viewsets/charts/assets.py,sha256=shhM9O9qa4fwX3BpqBnfqhZEyPrmCwM81e56cUsqz9g,18013
|
|
462
463
|
wbportfolio/viewsets/configs/__init__.py,sha256=K5opfVQvgGwSyw3XwDGkv3lo5i9jBxOJXYhLqyCdfeQ,137
|
|
463
464
|
wbportfolio/viewsets/configs/buttons/__init__.py,sha256=hyvpxwh9ss8Q0xcE006qsTLuSsJeWt9saHRcU4g96tA,791
|
|
464
465
|
wbportfolio/viewsets/configs/buttons/adjustments.py,sha256=sUY_3vxqP0kuqs8i5hklfboZI6QiAOrmu30eb29Xupo,492
|
|
@@ -548,7 +549,7 @@ wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSW
|
|
|
548
549
|
wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
|
|
549
550
|
wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
|
|
550
551
|
wbportfolio/viewsets/orders/order_proposals.py,sha256=IMiRh7kFqzmHjC34k-FjV21NSxkWVBi5NTEtUysN9qg,8302
|
|
551
|
-
wbportfolio/viewsets/orders/orders.py,sha256=
|
|
552
|
+
wbportfolio/viewsets/orders/orders.py,sha256=O6Mo5t18FEGDLzAZZvgl8PY1STLiLgv1fyYVhupuSSY,11678
|
|
552
553
|
wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
|
|
553
554
|
wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
|
|
554
555
|
wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=1BPkIYv0-K2DDGa4Gua2_Pxsx7fNurTZ2tYNdL66On0,6495
|
|
@@ -566,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
|
|
|
566
567
|
wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
|
|
567
568
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
568
569
|
wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
|
|
569
|
-
wbportfolio-1.
|
|
570
|
-
wbportfolio-1.
|
|
571
|
-
wbportfolio-1.
|
|
572
|
-
wbportfolio-1.
|
|
570
|
+
wbportfolio-1.56.0.dist-info/METADATA,sha256=NhAeOSJy0xO23T7bg0jlXtyKwSI-0rxA50MljwOL0eY,751
|
|
571
|
+
wbportfolio-1.56.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
572
|
+
wbportfolio-1.56.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
573
|
+
wbportfolio-1.56.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|