moexapi 1.6.42__tar.gz → 1.6.44__tar.gz
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.
- {moexapi-1.6.42 → moexapi-1.6.44}/PKG-INFO +1 -1
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/bonds.py +48 -1
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/tickers.py +16 -2
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi.egg-info/PKG-INFO +1 -1
- {moexapi-1.6.42 → moexapi-1.6.44}/pyproject.toml +1 -1
- {moexapi-1.6.42 → moexapi-1.6.44}/LICENSE +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/README.md +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/__init__.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/candles.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/changeover.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/dividends.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/exchange.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/history.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/markets.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/splits.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi/utils.py +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi.egg-info/SOURCES.txt +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi.egg-info/dependency_links.txt +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi.egg-info/requires.txt +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/moexapi.egg-info/top_level.txt +0 -0
- {moexapi-1.6.42 → moexapi-1.6.44}/setup.cfg +0 -0
|
@@ -7,6 +7,9 @@ from . import tickers
|
|
|
7
7
|
from . import utils
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
logger = utils.initialize_logging(__file__)
|
|
11
|
+
|
|
12
|
+
|
|
10
13
|
def _max(first, second):
|
|
11
14
|
if first is not None and second is not None:
|
|
12
15
|
return max(first, second)
|
|
@@ -120,13 +123,57 @@ class Bond:
|
|
|
120
123
|
self.amortization = [item for item in self.amortization if item.date != start_date]
|
|
121
124
|
self.coupons = [item for item in self.coupons if item.date != start_date]
|
|
122
125
|
self.offers = [item for item in self.offers if item.date != start_date]
|
|
126
|
+
original_values = [item.value for item in self.amortization]
|
|
127
|
+
amortization_sum = sum(original_values)
|
|
128
|
+
if abs(amortization_sum - self.initial_face_value) > 1e-9:
|
|
129
|
+
values = [value / amortization_sum * self.initial_face_value for value in original_values]
|
|
130
|
+
rounded_values = [round(value + 1e-9, 2) for value in values]
|
|
131
|
+
for original_value, rounded_value in zip(original_values, rounded_values):
|
|
132
|
+
if abs(original_value - rounded_value) > 1e-9:
|
|
133
|
+
logger.error(f"Original value {original_value} is not equal to rounded value {rounded_value}")
|
|
134
|
+
raise ValueError(f"Amortization sum {amortization_sum} is greater than initial face value {self.initial_face_value}")
|
|
135
|
+
for amortization_item, value in zip(self.amortization, values):
|
|
136
|
+
amortization_item.value = value
|
|
123
137
|
except Exception as e:
|
|
124
|
-
|
|
138
|
+
logger.error(f"{ticker.secid} ({ticker.shortname}): {e}")
|
|
139
|
+
raise e
|
|
125
140
|
|
|
126
141
|
@property
|
|
127
142
|
def expiration_date(self) -> datetime.date:
|
|
128
143
|
return max(item.date for item in self.amortization + self.coupons + self.offers)
|
|
129
144
|
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
lines = [
|
|
147
|
+
f"Bond({self.secid})",
|
|
148
|
+
f" name: {self.name}",
|
|
149
|
+
f" shortname: {self.shortname}",
|
|
150
|
+
f" issue_date: {self.issue_date}",
|
|
151
|
+
f" mat_date: {self.mat_date}",
|
|
152
|
+
f" initial_face_value: {self.initial_face_value}",
|
|
153
|
+
f" face_value: {self.face_value}",
|
|
154
|
+
f" start_date_moex: {self.start_date_moex}",
|
|
155
|
+
f" early_repayment: {self.early_repayment}",
|
|
156
|
+
f" days_to_redemption: {self.days_to_redemption}",
|
|
157
|
+
f" issue_size: {self.issue_size:,}",
|
|
158
|
+
f" is_qualified_investors:{' ' if not self.is_qualified_investors else ''}{self.is_qualified_investors}",
|
|
159
|
+
f" coupon_frequency: {self.coupon_frequency}",
|
|
160
|
+
f" evening_session: {self.evening_session}",
|
|
161
|
+
f" coupon_percent: {self.coupon_percent}",
|
|
162
|
+
]
|
|
163
|
+
if self.amortization:
|
|
164
|
+
lines.append(f" amortization ({len(self.amortization)}):")
|
|
165
|
+
for a in self.amortization:
|
|
166
|
+
lines.append(f" {a.date} value={a.value} face={a.initialfacevalue}")
|
|
167
|
+
if self.coupons:
|
|
168
|
+
lines.append(f" coupons ({len(self.coupons)}):")
|
|
169
|
+
for c in self.coupons:
|
|
170
|
+
lines.append(f" {c.date} value={c.value}")
|
|
171
|
+
if self.offers:
|
|
172
|
+
lines.append(f" offers ({len(self.offers)}):")
|
|
173
|
+
for o in self.offers:
|
|
174
|
+
lines.append(f" {o.date} value={o.value}")
|
|
175
|
+
return "\n".join(lines)
|
|
176
|
+
|
|
130
177
|
def next_offer(self, date_from: T.Optional[datetime.date] = None) -> T.Optional[Offer]:
|
|
131
178
|
date_from = date_from or datetime.date.today()
|
|
132
179
|
result = [offer for offer in self.offers if offer.date >= date_from]
|
|
@@ -28,6 +28,7 @@ ACCRUEDINT = "ACCRUEDINT"
|
|
|
28
28
|
VALTODAY = "VALTODAY"
|
|
29
29
|
CURRENCY = "CURRENCYID"
|
|
30
30
|
IS_TRADED = "is_traded"
|
|
31
|
+
LISTED_TILL = "listed_till"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def _sur_to_rub(currency: T.Optional[str]) -> T.Optional[str]:
|
|
@@ -145,22 +146,29 @@ class TickerInfo:
|
|
|
145
146
|
isin: T.Optional[str]
|
|
146
147
|
subtype: T.Optional[str]
|
|
147
148
|
listlevel: T.Optional[int]
|
|
149
|
+
listed_till: T.Optional[datetime.date] = None
|
|
148
150
|
|
|
149
151
|
@classmethod
|
|
150
152
|
def from_secid(cls, secid: str, market: markets.Market) -> "TickerInfo":
|
|
151
153
|
response = utils.json_api_call(f"https://iss.moex.com/iss/securities/{secid}.json")
|
|
152
154
|
boards = utils.prepare_dict(response, "boards")
|
|
153
155
|
is_traded = False
|
|
156
|
+
listed_till = datetime.date.min
|
|
154
157
|
for line in boards:
|
|
155
158
|
if not market.boards or line[BOARDID.lower()] in market.boards and line[IS_TRADED] == 1:
|
|
156
159
|
is_traded = True
|
|
160
|
+
if line[LISTED_TILL] is not None and listed_till is not None:
|
|
161
|
+
listed_till = max(listed_till, datetime.date.fromisoformat(line[LISTED_TILL]))
|
|
162
|
+
else:
|
|
163
|
+
listed_till = None
|
|
157
164
|
description = get_ticker_info_dict(secid)
|
|
158
165
|
return cls(
|
|
159
166
|
shortname=description.get(SHORTNAME),
|
|
160
167
|
isin=description.get(ISIN),
|
|
161
168
|
subtype=description.get(SECSUBTYPE),
|
|
162
169
|
listlevel=int(description[LISTLEVEL]) if LISTLEVEL in description else None,
|
|
163
|
-
is_traded=is_traded
|
|
170
|
+
is_traded=is_traded,
|
|
171
|
+
listed_till=listed_till,
|
|
164
172
|
)
|
|
165
173
|
|
|
166
174
|
|
|
@@ -181,7 +189,7 @@ class Ticker:
|
|
|
181
189
|
price_in_rub: T.Optional[float] = None
|
|
182
190
|
accumulated_coupon: T.Optional[float] = None
|
|
183
191
|
value: T.Optional[float] = None
|
|
184
|
-
|
|
192
|
+
listed_till: T.Optional[datetime.date] = None
|
|
185
193
|
@classmethod
|
|
186
194
|
def from_listing(cls, listing: Listing) -> "Ticker":
|
|
187
195
|
info = TickerInfo.from_secid(listing.secid, listing.market)
|
|
@@ -195,6 +203,7 @@ class Ticker:
|
|
|
195
203
|
isin=info.isin,
|
|
196
204
|
subtype=info.subtype,
|
|
197
205
|
listlevel=info.listlevel,
|
|
206
|
+
listed_till=info.listed_till,
|
|
198
207
|
)
|
|
199
208
|
board_info = TickerBoardInfo.from_secid(listing.secid, listing.market, listing.board)
|
|
200
209
|
if board_info is not None:
|
|
@@ -238,6 +247,11 @@ class Ticker:
|
|
|
238
247
|
if ticker not in unique_tickers:
|
|
239
248
|
unique_tickers.append(ticker)
|
|
240
249
|
tickers = unique_tickers
|
|
250
|
+
if len(tickers) > 1 and any(ticker.listed_till is None for ticker in tickers):
|
|
251
|
+
tickers = [ticker for ticker in tickers if ticker.listed_till is None]
|
|
252
|
+
if len(tickers) > 1:
|
|
253
|
+
max_listed_till = max(ticker.listed_till for ticker in tickers)
|
|
254
|
+
tickers = [ticker for ticker in tickers if ticker.listed_till == max_listed_till]
|
|
241
255
|
if len(tickers) != 1:
|
|
242
256
|
if len(tickers) > 1:
|
|
243
257
|
logger.error(f'Find too many tickers for {secid}: {tickers}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|