wbportfolio 1.54.3__py2.py3-none-any.whl → 1.54.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/import_export/handlers/trade.py +3 -3
- wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
- wbportfolio/models/asset.py +19 -18
- wbportfolio/models/portfolio.py +100 -90
- wbportfolio/models/transactions/rebalancing.py +22 -12
- wbportfolio/models/transactions/trade_proposals.py +193 -77
- wbportfolio/models/transactions/trades.py +36 -19
- wbportfolio/pms/analytics/portfolio.py +5 -6
- wbportfolio/pms/trading/handler.py +16 -19
- wbportfolio/pms/typing.py +26 -11
- wbportfolio/rebalancing/base.py +12 -1
- wbportfolio/rebalancing/models/equally_weighted.py +10 -13
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +18 -9
- wbportfolio/serializers/transactions/trade_proposals.py +2 -2
- wbportfolio/tests/models/test_portfolios.py +5 -3
- wbportfolio/tests/models/transactions/test_trade_proposals.py +13 -12
- wbportfolio/tests/pms/test_analytics.py +4 -3
- wbportfolio/tests/rebalancing/test_models.py +16 -10
- wbportfolio/viewsets/configs/display/trade_proposals.py +9 -0
- wbportfolio/viewsets/transactions/trade_proposals.py +1 -1
- {wbportfolio-1.54.3.dist-info → wbportfolio-1.54.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.3.dist-info → wbportfolio-1.54.4.dist-info}/RECORD +24 -23
- {wbportfolio-1.54.3.dist-info → wbportfolio-1.54.4.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.3.dist-info → wbportfolio-1.54.4.dist-info}/licenses/LICENSE +0 -0
wbportfolio/pms/typing.py
CHANGED
|
@@ -19,7 +19,7 @@ class Position:
|
|
|
19
19
|
weighting: Decimal
|
|
20
20
|
date: date_lib
|
|
21
21
|
|
|
22
|
-
drift_factor:
|
|
22
|
+
drift_factor: Decimal = Decimal("1")
|
|
23
23
|
currency: int | None = None
|
|
24
24
|
instrument_type: int | None = None
|
|
25
25
|
asset_valuation_date: date_lib | None = None
|
|
@@ -45,10 +45,15 @@ class Position:
|
|
|
45
45
|
**{f.name: getattr(self, f.name) for f in fields(Position) if f.name not in ["weighting", "shares"]},
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
+
def copy(self, **kwargs):
|
|
49
|
+
attrs = {f.name: getattr(self, f.name) for f in fields(Position)}
|
|
50
|
+
attrs.update(kwargs)
|
|
51
|
+
return Position(**attrs)
|
|
52
|
+
|
|
48
53
|
|
|
49
54
|
@dataclass(frozen=True)
|
|
50
55
|
class Portfolio:
|
|
51
|
-
positions:
|
|
56
|
+
positions: list[Position] | list
|
|
52
57
|
positions_map: dict[int, Position] = field(init=False, repr=False)
|
|
53
58
|
|
|
54
59
|
def __post_init__(self):
|
|
@@ -62,7 +67,7 @@ class Portfolio:
|
|
|
62
67
|
|
|
63
68
|
@property
|
|
64
69
|
def total_weight(self):
|
|
65
|
-
return round(sum([pos.weighting for pos in self.positions]),
|
|
70
|
+
return round(sum([pos.weighting for pos in self.positions]), 8)
|
|
66
71
|
|
|
67
72
|
@property
|
|
68
73
|
def total_shares(self):
|
|
@@ -77,6 +82,9 @@ class Portfolio:
|
|
|
77
82
|
def __len__(self):
|
|
78
83
|
return len(self.positions)
|
|
79
84
|
|
|
85
|
+
def __bool__(self):
|
|
86
|
+
return len(self.positions) > 0
|
|
87
|
+
|
|
80
88
|
|
|
81
89
|
@dataclass(frozen=True)
|
|
82
90
|
class Trade:
|
|
@@ -85,7 +93,7 @@ class Trade:
|
|
|
85
93
|
currency: int
|
|
86
94
|
date: date_lib
|
|
87
95
|
price: Decimal
|
|
88
|
-
|
|
96
|
+
previous_weight: Decimal
|
|
89
97
|
target_weight: Decimal
|
|
90
98
|
currency_fx_rate: Decimal = Decimal("1")
|
|
91
99
|
effective_shares: Decimal = Decimal("0")
|
|
@@ -97,20 +105,22 @@ class Trade:
|
|
|
97
105
|
def __add__(self, other):
|
|
98
106
|
return Trade(
|
|
99
107
|
underlying_instrument=self.underlying_instrument,
|
|
100
|
-
|
|
108
|
+
previous_weight=self.previous_weight,
|
|
101
109
|
target_weight=self.target_weight + other.target_weight,
|
|
102
110
|
effective_shares=self.effective_shares,
|
|
103
111
|
target_shares=self.target_shares + other.target_shares,
|
|
104
|
-
|
|
112
|
+
drift_factor=self.drift_factor
|
|
113
|
+
** {
|
|
105
114
|
f.name: getattr(self, f.name)
|
|
106
115
|
for f in fields(Trade)
|
|
107
116
|
if f.name
|
|
108
117
|
not in [
|
|
109
|
-
"
|
|
118
|
+
"previous_weight",
|
|
110
119
|
"target_weight",
|
|
111
120
|
"effective_shares",
|
|
112
121
|
"target_shares",
|
|
113
122
|
"underlying_instrument",
|
|
123
|
+
"drift_factor",
|
|
114
124
|
]
|
|
115
125
|
},
|
|
116
126
|
)
|
|
@@ -120,6 +130,10 @@ class Trade:
|
|
|
120
130
|
attrs.update(kwargs)
|
|
121
131
|
return Trade(**attrs)
|
|
122
132
|
|
|
133
|
+
@property
|
|
134
|
+
def effective_weight(self) -> Decimal:
|
|
135
|
+
return self.previous_weight * self.drift_factor
|
|
136
|
+
|
|
123
137
|
@property
|
|
124
138
|
def delta_weight(self) -> Decimal:
|
|
125
139
|
return self.target_weight - self.effective_weight
|
|
@@ -168,11 +182,11 @@ class TradeBatch:
|
|
|
168
182
|
|
|
169
183
|
@property
|
|
170
184
|
def total_target_weight(self) -> Decimal:
|
|
171
|
-
return round(sum([trade.target_weight for trade in self.trades], Decimal("0")),
|
|
185
|
+
return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 8)
|
|
172
186
|
|
|
173
187
|
@property
|
|
174
188
|
def total_effective_weight(self) -> Decimal:
|
|
175
|
-
return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")),
|
|
189
|
+
return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 8)
|
|
176
190
|
|
|
177
191
|
@property
|
|
178
192
|
def total_abs_delta_weight(self) -> Decimal:
|
|
@@ -188,14 +202,15 @@ class TradeBatch:
|
|
|
188
202
|
if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
|
|
189
203
|
raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
|
|
190
204
|
|
|
191
|
-
def convert_to_portfolio(self, *extra_positions):
|
|
205
|
+
def convert_to_portfolio(self, use_effective: bool = False, *extra_positions):
|
|
192
206
|
positions = []
|
|
193
207
|
for instrument, trade in self.trades_map.items():
|
|
194
208
|
positions.append(
|
|
195
209
|
Position(
|
|
196
210
|
underlying_instrument=trade.underlying_instrument,
|
|
197
211
|
instrument_type=trade.instrument_type,
|
|
198
|
-
weighting=trade.target_weight,
|
|
212
|
+
weighting=trade.target_weight if not use_effective else trade.previous_weight,
|
|
213
|
+
drift_factor=trade.drift_factor if use_effective else Decimal("1"),
|
|
199
214
|
shares=trade.target_shares,
|
|
200
215
|
currency=trade.currency,
|
|
201
216
|
date=trade.date,
|
wbportfolio/rebalancing/base.py
CHANGED
|
@@ -8,10 +8,21 @@ class AbstractRebalancingModel:
|
|
|
8
8
|
def validation_errors(self) -> str:
|
|
9
9
|
return getattr(self, "_validation_errors", "Rebalacing cannot applied for these parameters")
|
|
10
10
|
|
|
11
|
-
def __init__(
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
portfolio,
|
|
14
|
+
trade_date: date,
|
|
15
|
+
last_effective_date: date,
|
|
16
|
+
effective_portfolio: PortfolioDTO | None = None,
|
|
17
|
+
**kwargs,
|
|
18
|
+
):
|
|
12
19
|
self.portfolio = portfolio
|
|
13
20
|
self.trade_date = trade_date
|
|
14
21
|
self.last_effective_date = last_effective_date
|
|
22
|
+
self.effective_portfolio = effective_portfolio
|
|
23
|
+
# we try to get the portfolio at the trade date
|
|
24
|
+
if not self.effective_portfolio:
|
|
25
|
+
self.effective_portfolio = self.portfolio._build_dto(self.last_effective_date)
|
|
15
26
|
|
|
16
27
|
def is_valid(self) -> bool:
|
|
17
28
|
return True
|
|
@@ -9,24 +9,21 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
9
9
|
|
|
10
10
|
@register("Equally Weighted Rebalancing")
|
|
11
11
|
class EquallyWeightedRebalancing(AbstractRebalancingModel):
|
|
12
|
-
def __init__(self, *args, **kwargs):
|
|
13
|
-
super().__init__(*args, **kwargs)
|
|
14
|
-
self.assets = self.portfolio.assets.filter(date=self.last_effective_date)
|
|
15
|
-
|
|
16
12
|
def is_valid(self) -> bool:
|
|
17
13
|
return (
|
|
18
|
-
self.
|
|
14
|
+
len(self.effective_portfolio.positions) > 0
|
|
19
15
|
and InstrumentPrice.objects.filter(
|
|
20
|
-
date=self.trade_date, instrument__in=self.
|
|
16
|
+
date=self.trade_date, instrument__in=self.effective_portfolio.positions_map.keys()
|
|
21
17
|
).exists()
|
|
22
18
|
)
|
|
23
19
|
|
|
24
20
|
def get_target_portfolio(self) -> Portfolio:
|
|
25
21
|
positions = []
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
nb_assets = len(self.effective_portfolio.positions)
|
|
23
|
+
for position in self.effective_portfolio.positions:
|
|
24
|
+
positions.append(
|
|
25
|
+
position.copy(
|
|
26
|
+
weighting=Decimal(1 / nb_assets), date=self.trade_date, asset_valuation_date=self.trade_date
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
return Portfolio(positions)
|
|
@@ -13,6 +13,7 @@ from wbfdm.models import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
16
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
16
17
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
17
18
|
from wbportfolio.rebalancing.decorators import register
|
|
18
19
|
|
|
@@ -20,9 +21,17 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
20
21
|
@register("Market Capitalization Rebalancing")
|
|
21
22
|
class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
22
23
|
TARGET_CURRENCY: str = "USD"
|
|
24
|
+
MIN_WEIGHT: float = 10e-5 # we allow only weight of minimum 0.01%
|
|
23
25
|
|
|
24
|
-
def __init__(
|
|
25
|
-
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*args,
|
|
29
|
+
bypass_exchange_check: bool = False,
|
|
30
|
+
ffill_market_cap_limit: int = 5,
|
|
31
|
+
effective_portfolio: PortfolioDTO | None = None,
|
|
32
|
+
**kwargs,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(*args, effective_portfolio=effective_portfolio, **kwargs)
|
|
26
35
|
self.bypass_exchange_check = bypass_exchange_check
|
|
27
36
|
instruments = self._get_instruments(**kwargs)
|
|
28
37
|
self.market_cap_df = pd.DataFrame(
|
|
@@ -78,9 +87,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
78
87
|
)
|
|
79
88
|
|
|
80
89
|
if not instrument_ids:
|
|
81
|
-
instrument_ids = list(
|
|
82
|
-
self.portfolio.assets.filter(date=self.trade_date).values_list("underlying_instrument", flat=True)
|
|
83
|
-
)
|
|
90
|
+
instrument_ids = list(self.effective_portfolio.positions_map.keys())
|
|
84
91
|
|
|
85
92
|
return (
|
|
86
93
|
Instrument.objects.filter(id__in=instrument_ids)
|
|
@@ -110,12 +117,14 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
110
117
|
|
|
111
118
|
def get_target_portfolio(self) -> Portfolio:
|
|
112
119
|
positions = []
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
df = self.market_cap_df / self.market_cap_df.dropna().sum()
|
|
121
|
+
df = df[df > self.MIN_WEIGHT]
|
|
122
|
+
df = df / df.sum()
|
|
123
|
+
for underlying_instrument, weighting in df.to_dict().items():
|
|
124
|
+
if np.isnan(weighting):
|
|
116
125
|
weighting = Decimal(0)
|
|
117
126
|
else:
|
|
118
|
-
weighting = Decimal(
|
|
127
|
+
weighting = Decimal(weighting)
|
|
119
128
|
positions.append(
|
|
120
129
|
Position(
|
|
121
130
|
underlying_instrument=underlying_instrument,
|
|
@@ -43,8 +43,8 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
43
43
|
obj = super().create(validated_data)
|
|
44
44
|
|
|
45
45
|
target_portfolio_dto = None
|
|
46
|
-
if target_portfolio and not rebalancing_model
|
|
47
|
-
target_portfolio_dto = target_portfolio._build_dto(
|
|
46
|
+
if target_portfolio and not rebalancing_model:
|
|
47
|
+
target_portfolio_dto = target_portfolio._build_dto(obj.trade_date)
|
|
48
48
|
try:
|
|
49
49
|
obj.reset_trades(
|
|
50
50
|
target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
|
|
@@ -1011,7 +1011,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1011
1011
|
assert a.underlying_quote == instrument
|
|
1012
1012
|
assert a.underlying_quote_price == None
|
|
1013
1013
|
assert a.initial_price == p.net_value
|
|
1014
|
-
assert a.weighting == weights[instrument.id]
|
|
1014
|
+
assert a.weighting == pytest.approx(weights[instrument.id], abs=10e-6)
|
|
1015
1015
|
assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
|
|
1016
1016
|
assert a.currency_fx_rate_instrument_to_usd == fx_instrument
|
|
1017
1017
|
|
|
@@ -1047,7 +1047,9 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1047
1047
|
asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
|
|
1048
1048
|
|
|
1049
1049
|
rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
|
|
1050
|
-
positions, rebalancing_trade_proposal = portfolio.drift_weights(
|
|
1050
|
+
positions, rebalancing_trade_proposal = portfolio.drift_weights(
|
|
1051
|
+
weekday, (rebalancing_date + BDay(1)).date(), stop_at_rebalancing=True
|
|
1052
|
+
)
|
|
1051
1053
|
assert rebalancing_trade_proposal.trade_date == rebalancing_date
|
|
1052
1054
|
assert rebalancing_trade_proposal.status == "SUBMIT"
|
|
1053
1055
|
assert set(positions.get_weights().keys()) == {
|
|
@@ -1141,7 +1143,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1141
1143
|
i11.refresh_from_db()
|
|
1142
1144
|
i12.refresh_from_db()
|
|
1143
1145
|
i13.refresh_from_db()
|
|
1144
|
-
returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
|
|
1146
|
+
_, returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
|
|
1145
1147
|
|
|
1146
1148
|
expected_returns = pd.DataFrame(
|
|
1147
1149
|
[[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
|
|
@@ -131,15 +131,15 @@ class TestTradeProposal:
|
|
|
131
131
|
Verify the next trade proposal is correctly identified as the first approved proposal after the current one.
|
|
132
132
|
"""
|
|
133
133
|
tp = trade_proposal_factory.create()
|
|
134
|
+
tp_previous_approve = trade_proposal_factory.create( # noqa
|
|
135
|
+
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
|
|
136
|
+
)
|
|
134
137
|
tp_next_submit = trade_proposal_factory.create( # noqa
|
|
135
138
|
portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
|
|
136
139
|
)
|
|
137
140
|
tp_next_approve = trade_proposal_factory.create(
|
|
138
141
|
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
|
|
139
142
|
)
|
|
140
|
-
tp_previous_approve = trade_proposal_factory.create( # noqa
|
|
141
|
-
portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
|
|
142
|
-
)
|
|
143
143
|
|
|
144
144
|
# The next valid trade proposal should be the approved one strictly after the current proposal
|
|
145
145
|
assert (
|
|
@@ -236,9 +236,9 @@ class TestTradeProposal:
|
|
|
236
236
|
t3.refresh_from_db()
|
|
237
237
|
|
|
238
238
|
# Expected normalized weights
|
|
239
|
-
normalized_t1_weight = Decimal("0.
|
|
240
|
-
normalized_t2_weight = Decimal("0.
|
|
241
|
-
normalized_t3_weight = Decimal("0.
|
|
239
|
+
normalized_t1_weight = Decimal("0.33333333")
|
|
240
|
+
normalized_t2_weight = Decimal("0.43333333")
|
|
241
|
+
normalized_t3_weight = Decimal("0.23333333")
|
|
242
242
|
|
|
243
243
|
# Calculate quantization error
|
|
244
244
|
quantize_error = Decimal(1) - (normalized_t1_weight + normalized_t2_weight + normalized_t3_weight)
|
|
@@ -362,9 +362,9 @@ class TestTradeProposal:
|
|
|
362
362
|
|
|
363
363
|
# Expected calls to drift_weights
|
|
364
364
|
expected_calls = [
|
|
365
|
-
call(tp0.trade_date, tp1.trade_date - timedelta(days=1)),
|
|
366
|
-
call(tp1.trade_date, tp2.trade_date - timedelta(days=1)),
|
|
367
|
-
call(tp2.trade_date, date.today()),
|
|
365
|
+
call(tp0.trade_date, tp1.trade_date - timedelta(days=1), stop_at_rebalancing=True),
|
|
366
|
+
call(tp1.trade_date, tp2.trade_date - timedelta(days=1), stop_at_rebalancing=True),
|
|
367
|
+
call(tp2.trade_date, date.today(), stop_at_rebalancing=True),
|
|
368
368
|
]
|
|
369
369
|
|
|
370
370
|
# Assert drift_weights was called as expected
|
|
@@ -373,7 +373,7 @@ class TestTradeProposal:
|
|
|
373
373
|
# Test stopping replay on a non-approved proposal
|
|
374
374
|
tp1.status = TradeProposal.Status.FAILED
|
|
375
375
|
tp1.save()
|
|
376
|
-
expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1))]
|
|
376
|
+
expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1), stop_at_rebalancing=True)]
|
|
377
377
|
mock_fct.assert_has_calls(expected_calls)
|
|
378
378
|
|
|
379
379
|
# Test estimating shares for a trade
|
|
@@ -577,11 +577,9 @@ class TestTradeProposal:
|
|
|
577
577
|
# --- Create a trade proposal on d3 ---
|
|
578
578
|
trade_proposal = trade_proposal_factory.create(portfolio=portfolio, trade_date=d3)
|
|
579
579
|
trade_proposal.reset_trades()
|
|
580
|
-
|
|
581
580
|
# Retrieve trades for each instrument
|
|
582
581
|
trade_msft = trade_proposal.trades.get(underlying_instrument=msft)
|
|
583
582
|
trade_apple = trade_proposal.trades.get(underlying_instrument=apple)
|
|
584
|
-
|
|
585
583
|
# Check that trade weights are initially zero
|
|
586
584
|
assert trade_msft.weighting == Decimal("0")
|
|
587
585
|
assert trade_apple.weighting == Decimal("0")
|
|
@@ -627,3 +625,6 @@ class TestTradeProposal:
|
|
|
627
625
|
# Final check that weights have been updated to 50%
|
|
628
626
|
assert msft_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
|
|
629
627
|
assert apple_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
|
|
628
|
+
|
|
629
|
+
def test_invalid_future_trade_proposal(self, trade_proposal):
|
|
630
|
+
pass
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
|
+
import pytest
|
|
2
3
|
|
|
3
4
|
from wbportfolio.pms.analytics.portfolio import Portfolio
|
|
4
5
|
|
|
@@ -15,9 +16,9 @@ def test_get_next_weights():
|
|
|
15
16
|
portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
|
|
16
17
|
next_weights = portfolio.get_next_weights()
|
|
17
18
|
|
|
18
|
-
assert next_weights[0] == w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
|
|
19
|
-
assert next_weights[1] == w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
|
|
20
|
-
assert next_weights[2] == w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
|
|
19
|
+
assert next_weights[0] == pytest.approx(w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
|
|
20
|
+
assert next_weights[1] == pytest.approx(w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
|
|
21
|
+
assert next_weights[2] == pytest.approx(w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def test_get_estimate_net_value():
|
|
@@ -10,22 +10,28 @@ from wbportfolio.models import PortfolioPortfolioThroughModel, Trade, TradePropo
|
|
|
10
10
|
|
|
11
11
|
@pytest.mark.django_db
|
|
12
12
|
class TestEquallyWeightedRebalancing:
|
|
13
|
-
|
|
14
|
-
def model(self, portfolio, weekday):
|
|
13
|
+
def test_is_valid(self, portfolio, weekday, asset_position_factory, instrument_price_factory):
|
|
15
14
|
from wbportfolio.rebalancing.models import EquallyWeightedRebalancing
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
|
|
16
|
+
trade_date = (weekday + BDay(1)).date()
|
|
17
|
+
model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
|
|
20
18
|
assert not model.is_valid()
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
a = asset_position_factory.create(portfolio=portfolio, date=weekday)
|
|
21
|
+
model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
|
|
22
22
|
assert not model.is_valid()
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
instrument_price_factory.create(instrument=a.underlying_quote, date=trade_date)
|
|
25
|
+
model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
|
|
24
26
|
assert model.is_valid()
|
|
25
27
|
|
|
26
|
-
def test_get_target_portfolio(self, portfolio, weekday,
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
def test_get_target_portfolio(self, portfolio, weekday, asset_position_factory):
|
|
29
|
+
from wbportfolio.rebalancing.models import EquallyWeightedRebalancing
|
|
30
|
+
|
|
31
|
+
a1 = asset_position_factory(weighting=0.7, portfolio=portfolio, date=weekday)
|
|
32
|
+
a2 = asset_position_factory(weighting=0.3, portfolio=portfolio, date=weekday)
|
|
33
|
+
model = EquallyWeightedRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
|
|
34
|
+
|
|
29
35
|
target_portfolio = model.get_target_portfolio()
|
|
30
36
|
target_positions = target_portfolio.positions_map
|
|
31
37
|
assert target_positions[a1.underlying_instrument.id].weighting == Decimal(0.5)
|
|
@@ -41,6 +41,11 @@ class TradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
41
41
|
label=TradeProposal.Status.DENIED.label,
|
|
42
42
|
value=TradeProposal.Status.DENIED.value,
|
|
43
43
|
),
|
|
44
|
+
dp.LegendItem(
|
|
45
|
+
icon=WBColor.RED_DARK.value,
|
|
46
|
+
label=TradeProposal.Status.FAILED.label,
|
|
47
|
+
value=TradeProposal.Status.FAILED.value,
|
|
48
|
+
),
|
|
44
49
|
],
|
|
45
50
|
),
|
|
46
51
|
],
|
|
@@ -64,6 +69,10 @@ class TradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
64
69
|
style={"backgroundColor": WBColor.RED_LIGHT.value},
|
|
65
70
|
condition=("==", TradeProposal.Status.DENIED.value),
|
|
66
71
|
),
|
|
72
|
+
dp.FormattingRule(
|
|
73
|
+
style={"backgroundColor": WBColor.RED_DARK.value},
|
|
74
|
+
condition=("==", TradeProposal.Status.FAILED.value),
|
|
75
|
+
),
|
|
67
76
|
],
|
|
68
77
|
)
|
|
69
78
|
],
|
|
@@ -100,7 +100,7 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
100
100
|
trade_proposal = get_object_or_404(TradeProposal, pk=pk)
|
|
101
101
|
if trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
102
102
|
trade_proposal.trades.all().delete()
|
|
103
|
-
trade_proposal.reset_trades()
|
|
103
|
+
trade_proposal.reset_trades(force_reset_trade=True)
|
|
104
104
|
return Response({"send": True})
|
|
105
105
|
return Response({"status": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
106
106
|
|
|
@@ -113,7 +113,7 @@ wbportfolio/import_export/handlers/dividend.py,sha256=F0oLfNt2B_QQAjHBCRpxa5HSkf
|
|
|
113
113
|
wbportfolio/import_export/handlers/fees.py,sha256=BOFHAvSTlvVLaxnm6KD_fcza1TlPc02HOR9J0_jjswI,2495
|
|
114
114
|
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
|
|
115
115
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
116
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
116
|
+
wbportfolio/import_export/handlers/trade.py,sha256=t-iezNZN6s834EsszGh_5sHnFGgLb3MZpcHrF2pWMG8,12533
|
|
117
117
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
118
118
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
|
|
119
119
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -249,14 +249,15 @@ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4
|
|
|
249
249
|
wbportfolio/migrations/0077_remove_transaction_currency_and_more.py,sha256=Yf4a3zn2siDtWdIEPEIsj_W87jxOIBwiFVATneU8FxU,29597
|
|
250
250
|
wbportfolio/migrations/0078_trade_drift_factor.py,sha256=26Z3yoiBhMueB-k2R9HaIzg5Qr7BYpdtzlU-65T_cH0,999
|
|
251
251
|
wbportfolio/migrations/0079_alter_trade_drift_factor.py,sha256=2tvPecUxEy60-ELy9wtiuTR2dhJ8HucJjvOEuia4Pp4,627
|
|
252
|
+
wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py,sha256=vCPJSrM7x24jUJseGkHEVBD0c5nEpDTrb9-zkJ-0QoI,569
|
|
252
253
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
253
254
|
wbportfolio/models/__init__.py,sha256=HSpa5xwh_MHQaBpNrq9E0CbdEE5Iq-pDLIsPzZ-TRTg,904
|
|
254
255
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
255
|
-
wbportfolio/models/asset.py,sha256=
|
|
256
|
+
wbportfolio/models/asset.py,sha256=hhLv3Gyzr3-yd2bFA6wwYZ_i9XuqswzUIexdW1XKPWw,45634
|
|
256
257
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
257
258
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
258
259
|
wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
|
|
259
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
260
|
+
wbportfolio/models/portfolio.py,sha256=Jp6MGZten0kKzNUmzD_2_NVSPWUY17L4eF3y8bZwynU,57506
|
|
260
261
|
wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
|
|
261
262
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
262
263
|
wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
|
|
@@ -282,24 +283,24 @@ wbportfolio/models/transactions/__init__.py,sha256=aV6lehRHSs8cOKOanm6UgSDqOwEzR
|
|
|
282
283
|
wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWKAE6AY6YOYTs,25875
|
|
283
284
|
wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
|
|
284
285
|
wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
|
|
285
|
-
wbportfolio/models/transactions/rebalancing.py,sha256=
|
|
286
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
287
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
286
|
+
wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
|
|
287
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=33RLqsknglMsESUb_d0gt0ehwyDIn3i4s7nOdyvsi5E,37832
|
|
288
|
+
wbportfolio/models/transactions/trades.py,sha256=7btwshl10-XVOrTYF9PXNyfuHGOttA11D0hkvOLEhgs,34126
|
|
288
289
|
wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
|
|
289
290
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
290
|
-
wbportfolio/pms/typing.py,sha256=
|
|
291
|
+
wbportfolio/pms/typing.py,sha256=BV4dzazNHdfpfLV99bLVyYGcETmbQSnFV6ipc4fNKfg,8470
|
|
291
292
|
wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
292
|
-
wbportfolio/pms/analytics/portfolio.py,sha256=
|
|
293
|
+
wbportfolio/pms/analytics/portfolio.py,sha256=n_tv4gX-mOFfKCxhSGblqQzW62Bx-yoM2QcJxQd3dLE,1384
|
|
293
294
|
wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
294
295
|
wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
|
|
295
|
-
wbportfolio/pms/trading/handler.py,sha256=
|
|
296
|
+
wbportfolio/pms/trading/handler.py,sha256=ZOwgnOU4ScVIhTMRQ0SLR2cCCZP9whmVv-S5hF-TOME,8593
|
|
296
297
|
wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
297
|
-
wbportfolio/rebalancing/base.py,sha256=
|
|
298
|
+
wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
|
|
298
299
|
wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
|
|
299
300
|
wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
|
|
300
301
|
wbportfolio/rebalancing/models/composite.py,sha256=XEgK3oMurrE_d_l5uN0stBKRrtvnKQzRWyXNXuBYfmc,1818
|
|
301
|
-
wbportfolio/rebalancing/models/equally_weighted.py,sha256=
|
|
302
|
-
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=
|
|
302
|
+
wbportfolio/rebalancing/models/equally_weighted.py,sha256=nqkiCnDfazC0AZeRPMVtqRzbDHdW8P2FCMYXEvmwOMw,1062
|
|
303
|
+
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=BJUoJRQ-qTG3cevPKm_3HMy4lVNk6wuxHGuUobPkrMY,5523
|
|
303
304
|
wbportfolio/rebalancing/models/model_portfolio.py,sha256=Dk3tw9u3WG1jCP3V2-R05GS4-DmDBBtxH4h6p7pRe4g,1393
|
|
304
305
|
wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
305
306
|
wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
|
|
@@ -348,7 +349,7 @@ wbportfolio/serializers/transactions/__init__.py,sha256=oAfidhjjCKP0exeHbzJgGuBd
|
|
|
348
349
|
wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3ikR5F1Wf0v_IGIvw,11599
|
|
349
350
|
wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
|
|
350
351
|
wbportfolio/serializers/transactions/fees.py,sha256=3mBzs6vdfST9roeQB-bmLJhipY7i5jBtAXjoTTE-GOg,2388
|
|
351
|
-
wbportfolio/serializers/transactions/trade_proposals.py,sha256=
|
|
352
|
+
wbportfolio/serializers/transactions/trade_proposals.py,sha256=2oX04DSyiQ5C0-XkB7c0_wDJ-yC0niRgC72pXPPa_Xc,4014
|
|
352
353
|
wbportfolio/serializers/transactions/trades.py,sha256=YVccQpP480P4-0uVaRfnmpPFoIdW2U0c92kJBR_fPLo,16889
|
|
353
354
|
wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
|
|
354
355
|
wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md,sha256=MabxOvOne8s5gl6osDoow6-3ghaXLAYg9THWpvy6G5I,921
|
|
@@ -379,7 +380,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
379
380
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
380
381
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
381
382
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
382
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
383
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=sGmPrl-nY1-3797SDTRWAGF2xXZGD94QS3WeabfBmC0,53397
|
|
383
384
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
384
385
|
wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
|
|
385
386
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -389,12 +390,12 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
389
390
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
390
391
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
391
392
|
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
|
|
392
|
-
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=
|
|
393
|
+
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=jnE5Ie0g05TVkuSE9uKsqfidYDU2ZN3FLg1aveLzj9c,28558
|
|
393
394
|
wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
|
|
394
395
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
395
|
-
wbportfolio/tests/pms/test_analytics.py,sha256=
|
|
396
|
+
wbportfolio/tests/pms/test_analytics.py,sha256=WHicJBjAjpIRL1-AW2nZ4VD9oJRpMoeH6V1Qx2D95-w,1178
|
|
396
397
|
wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
397
|
-
wbportfolio/tests/rebalancing/test_models.py,sha256=
|
|
398
|
+
wbportfolio/tests/rebalancing/test_models.py,sha256=QMfcYDvFew1bH6kPm-jVJLC_RqmPE-oGTqUldx1KVgg,8025
|
|
398
399
|
wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
399
400
|
wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
|
|
400
401
|
wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -458,7 +459,7 @@ wbportfolio/viewsets/configs/display/rebalancing.py,sha256=yw9X1Nf2-V_KP_mCX4pVK
|
|
|
458
459
|
wbportfolio/viewsets/configs/display/reconciliations.py,sha256=YvMAuwmpX0HExvGsuf5UvcRQxe4eMo1iyNJX68GGC_k,6021
|
|
459
460
|
wbportfolio/viewsets/configs/display/registers.py,sha256=1np75exIk5rfct6UkVN_RnfJ9ozvIkcWJgFV4_4rJns,3182
|
|
460
461
|
wbportfolio/viewsets/configs/display/roles.py,sha256=SFUyCdxSlHZ3NsMrJmpVBSlg-XKGaEFteV89nyLMMAQ,1815
|
|
461
|
-
wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=
|
|
462
|
+
wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=EiPNzW-jntXsJnJQ3HSKSDQBjkjQERaEXP0DCRq6F5M,4826
|
|
462
463
|
wbportfolio/viewsets/configs/display/trades.py,sha256=ZYZ2ceE4Hyw2SpQjW00VDnzGz7Wlu3jjPoK_J2GZLyk,19181
|
|
463
464
|
wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=KR3AsSxl71VAVkYBRVowgs3PZB8vaKa34WHHUvC-2MY,2807
|
|
464
465
|
wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=KRZLqv4ZeB27d-_s7Qlqj9LI3I9WFJkjc6Mw8agDueE,606
|
|
@@ -519,9 +520,9 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
|
|
|
519
520
|
wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
|
|
520
521
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
521
522
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
522
|
-
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=
|
|
523
|
+
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=eDEIEUyMP70oEdwscnE8oR7pBStr7IjHNFMxu2JbJlY,6232
|
|
523
524
|
wbportfolio/viewsets/transactions/trades.py,sha256=GHOw5jtcqoaHiRrxxxL29c9405QiPisEn4coGELKDrE,22146
|
|
524
|
-
wbportfolio-1.54.
|
|
525
|
-
wbportfolio-1.54.
|
|
526
|
-
wbportfolio-1.54.
|
|
527
|
-
wbportfolio-1.54.
|
|
525
|
+
wbportfolio-1.54.4.dist-info/METADATA,sha256=N_ZubDWqgnEeWE58IyCdWzurZlbqE82G1vjnkT3dIok,702
|
|
526
|
+
wbportfolio-1.54.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
527
|
+
wbportfolio-1.54.4.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
528
|
+
wbportfolio-1.54.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|