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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moexapi
3
- Version: 1.6.42
3
+ Version: 1.6.44
4
4
  Summary: Python API for MOEX ISS
5
5
  Author-email: Aliaksei Rak <kventinel@gmail.com>
6
6
  License: MIT License
@@ -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
- raise type(e)(f"{ticker.secid} ({ticker.shortname}): {e}") from e
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}')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moexapi
3
- Version: 1.6.42
3
+ Version: 1.6.44
4
4
  Summary: Python API for MOEX ISS
5
5
  Author-email: Aliaksei Rak <kventinel@gmail.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moexapi"
7
- version = "1.6.42"
7
+ version = "1.6.44"
8
8
  description = "Python API for MOEX ISS"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Aliaksei Rak", email = "kventinel@gmail.com" }]
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