moexapi 1.7.20__tar.gz → 1.7.22__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.7.20
3
+ Version: 1.7.22
4
4
  Summary: Python API for MOEX ISS
5
5
  Author-email: Aliaksei Rak <kventinel@gmail.com>
6
6
  License: MIT License
@@ -66,6 +66,13 @@ Out[4]: [Candle(date=datetime.date(2023, 10, 2), low=165.51, high=168.86, open=1
66
66
 
67
67
  ### Bonds
68
68
 
69
+ `get_history()` returns `History.accrued_interest` from ISS `ACCRUEDINT`.
70
+ It is accrued coupon cash per security in `History.currency` (settlement
71
+ currency), not a percentage or a ruble conversion. Missing values remain `None`;
72
+ zero is a valid observation. Same-day board merges retain one matching value
73
+ and reject conflicting known values; if any merged observation is missing the
74
+ result is `None`. Split adjustments scale accrued cash with per-security prices.
75
+
69
76
  ```
70
77
  In [5]: moexapi.Bond(moexapi.get_ticker('SU26238RMFS4'))
71
78
  Out[5]: Bond(secid='SU26238RMFS4', shortname='ОФЗ 26238', amortization=[Amortization(date=datetime.date(2041, 5, 15), value=1000, initialfacevalue=1000)], coupons=[Coupon(date=2021-12-08, value=34.04), Coupon(date=2022-06-08, value=35.4), Coupon(date=2022-12-07, value=35.4), Coupon(date=2023-06-07, value=35.4), Coupon(date=2023-12-06, value=35.4), Coupon(date=2024-06-05, value=35.4), Coupon(date=2024-12-04, value=35.4), Coupon(date=2025-06-04, value=35.4), Coupon(date=2025-12-03, value=35.4), Coupon(date=2026-06-03, value=35.4), Coupon(date=2026-12-02, value=35.4), Coupon(date=2027-06-02, value=35.4), Coupon(date=2027-12-01, value=35.4), Coupon(date=2028-05-31, value=35.4), Coupon(date=2028-11-29, value=35.4), Coupon(date=2029-05-30, value=35.4), Coupon(date=2029-11-28, value=35.4), Coupon(date=2030-05-29, value=35.4), Coupon(date=2030-11-27, value=35.4), Coupon(date=2031-05-28, value=35.4), Coupon(date=2031-11-26, value=35.4), Coupon(date=2032-05-26, value=35.4), Coupon(date=2032-11-24, value=35.4), Coupon(date=2033-05-25, value=35.4), Coupon(date=2033-11-23, value=35.4), Coupon(date=2034-05-24, value=35.4), Coupon(date=2034-11-22, value=35.4), Coupon(date=2035-05-23, value=35.4), Coupon(date=2035-11-21, value=35.4), Coupon(date=2036-05-21, value=35.4), Coupon(date=2036-11-19, value=35.4), Coupon(date=2037-05-20, value=35.4), Coupon(date=2037-11-18, value=35.4), Coupon(date=2038-05-19, value=35.4), Coupon(date=2038-11-17, value=35.4), Coupon(date=2039-05-18, value=35.4), Coupon(date=2039-11-16, value=35.4), Coupon(date=2040-05-16, value=35.4), Coupon(date=2040-11-14, value=35.4), Coupon(date=2041-05-15, value=35.4)], offers=[])
@@ -26,6 +26,13 @@ Out[4]: [Candle(date=datetime.date(2023, 10, 2), low=165.51, high=168.86, open=1
26
26
 
27
27
  ### Bonds
28
28
 
29
+ `get_history()` returns `History.accrued_interest` from ISS `ACCRUEDINT`.
30
+ It is accrued coupon cash per security in `History.currency` (settlement
31
+ currency), not a percentage or a ruble conversion. Missing values remain `None`;
32
+ zero is a valid observation. Same-day board merges retain one matching value
33
+ and reject conflicting known values; if any merged observation is missing the
34
+ result is `None`. Split adjustments scale accrued cash with per-security prices.
35
+
29
36
  ```
30
37
  In [5]: moexapi.Bond(moexapi.get_ticker('SU26238RMFS4'))
31
38
  Out[5]: Bond(secid='SU26238RMFS4', shortname='ОФЗ 26238', amortization=[Amortization(date=datetime.date(2041, 5, 15), value=1000, initialfacevalue=1000)], coupons=[Coupon(date=2021-12-08, value=34.04), Coupon(date=2022-06-08, value=35.4), Coupon(date=2022-12-07, value=35.4), Coupon(date=2023-06-07, value=35.4), Coupon(date=2023-12-06, value=35.4), Coupon(date=2024-06-05, value=35.4), Coupon(date=2024-12-04, value=35.4), Coupon(date=2025-06-04, value=35.4), Coupon(date=2025-12-03, value=35.4), Coupon(date=2026-06-03, value=35.4), Coupon(date=2026-12-02, value=35.4), Coupon(date=2027-06-02, value=35.4), Coupon(date=2027-12-01, value=35.4), Coupon(date=2028-05-31, value=35.4), Coupon(date=2028-11-29, value=35.4), Coupon(date=2029-05-30, value=35.4), Coupon(date=2029-11-28, value=35.4), Coupon(date=2030-05-29, value=35.4), Coupon(date=2030-11-27, value=35.4), Coupon(date=2031-05-28, value=35.4), Coupon(date=2031-11-26, value=35.4), Coupon(date=2032-05-26, value=35.4), Coupon(date=2032-11-24, value=35.4), Coupon(date=2033-05-25, value=35.4), Coupon(date=2033-11-23, value=35.4), Coupon(date=2034-05-24, value=35.4), Coupon(date=2034-11-22, value=35.4), Coupon(date=2035-05-23, value=35.4), Coupon(date=2035-11-21, value=35.4), Coupon(date=2036-05-21, value=35.4), Coupon(date=2036-11-19, value=35.4), Coupon(date=2037-05-20, value=35.4), Coupon(date=2037-11-18, value=35.4), Coupon(date=2038-05-19, value=35.4), Coupon(date=2038-11-17, value=35.4), Coupon(date=2039-05-18, value=35.4), Coupon(date=2039-11-16, value=35.4), Coupon(date=2040-05-16, value=35.4), Coupon(date=2040-11-14, value=35.4), Coupon(date=2041-05-15, value=35.4)], offers=[])
@@ -139,12 +139,22 @@ class Bond:
139
139
  if abs(amortization_sum - self.initial_face_value) > 1e-9 and len(original_values) > 1:
140
140
  values = [value / amortization_sum * self.initial_face_value for value in original_values]
141
141
  rounded_values = [round(value + 1e-9, 2) for value in values]
142
- for original_value, rounded_value in zip(original_values, rounded_values):
143
- if abs(original_value - rounded_value) > 1e-9:
144
- logger.error(f"Original value {original_value} is not equal to rounded value {rounded_value}")
145
- raise ValueError(f"Amortization sum {amortization_sum} is greater than initial face value {self.initial_face_value}")
146
- for amortization_item, value in zip(self.amortization, values):
147
- amortization_item.value = value
142
+ if all(abs(original - rounded) <= 1e-9
143
+ for original, rounded in zip(original_values, rounded_values)):
144
+ # Correct only differences attributable to payment rounding.
145
+ for amortization_item, value in zip(self.amortization, values):
146
+ amortization_item.value = value
147
+ elif amortization_sum > self.initial_face_value:
148
+ raise ValueError(
149
+ f"Amortization sum {amortization_sum} is greater than initial face value {self.initial_face_value}"
150
+ )
151
+ else:
152
+ # Incomplete or discounted repayment schedules must retain
153
+ # their published cash flows rather than inventing principal.
154
+ logger.warning(
155
+ "%s: Amortization sum %s is less than initial face value %s; keeping published payments",
156
+ self.secid, amortization_sum, self.initial_face_value,
157
+ )
148
158
  except Exception as e:
149
159
  logger.error(f"{ticker.secid} ({ticker.shortname}): {e}")
150
160
  raise e
@@ -40,6 +40,8 @@ class History:
40
40
  volume -- number of shares/bonds/currencies sold on the day
41
41
  value -- sum of all transactions in currency for the day
42
42
  currency -- currency of transaction value
43
+ accrued_interest -- accrued coupon cash per security in currency, or None
44
+ when ISS does not supply it (not a percent quote)
43
45
  """
44
46
  date: datetime.date
45
47
  low: float
@@ -51,11 +53,18 @@ class History:
51
53
  volume: T.Optional[int]
52
54
  value: T.Optional[float]
53
55
  currency: T.Optional[str] = None
56
+ accrued_interest: T.Optional[float] = None
54
57
 
55
58
  @classmethod
56
59
  def merge(cls, first: 'History', second: 'History'):
57
60
  assert first.date == second.date
58
61
  assert first.currency == second.currency, "Can't merge history in different currencies"
62
+ accrued_interest = None
63
+ if first.accrued_interest is not None and second.accrued_interest is not None:
64
+ if not np.isclose(first.accrued_interest, second.accrued_interest):
65
+ raise ValueError(f"Conflicting accrued interest on {first.date}")
66
+ # This is cash per security, not turnover: never sum across boards.
67
+ accrued_interest = first.accrued_interest
59
68
  return cls(
60
69
  date=first.date,
61
70
  low=min(first.low, second.low),
@@ -67,6 +76,7 @@ class History:
67
76
  volume=_maybe_sum(first.volume, second.volume),
68
77
  value=_maybe_sum(first.value, second.value),
69
78
  currency=first.currency,
79
+ accrued_interest=accrued_interest,
70
80
  )
71
81
 
72
82
  def mult(self, mult: float) -> None:
@@ -75,6 +85,8 @@ class History:
75
85
  self.open *= mult
76
86
  self.close *= mult
77
87
  self.mid_price *= mult
88
+ if self.accrued_interest is not None:
89
+ self.accrued_interest *= mult
78
90
  if self.volume is not None:
79
91
  self.volume /= mult
80
92
 
@@ -156,6 +168,7 @@ def _parse_history(
156
168
  volume=line.get("VOLUME"),
157
169
  value=value,
158
170
  currency=currency,
171
+ accrued_interest=line.get("ACCRUEDINT"),
159
172
  )
160
173
  if len(result) > 0 and result[-1].date == date:
161
174
  if board in boards:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moexapi
3
- Version: 1.7.20
3
+ Version: 1.7.22
4
4
  Summary: Python API for MOEX ISS
5
5
  Author-email: Aliaksei Rak <kventinel@gmail.com>
6
6
  License: MIT License
@@ -66,6 +66,13 @@ Out[4]: [Candle(date=datetime.date(2023, 10, 2), low=165.51, high=168.86, open=1
66
66
 
67
67
  ### Bonds
68
68
 
69
+ `get_history()` returns `History.accrued_interest` from ISS `ACCRUEDINT`.
70
+ It is accrued coupon cash per security in `History.currency` (settlement
71
+ currency), not a percentage or a ruble conversion. Missing values remain `None`;
72
+ zero is a valid observation. Same-day board merges retain one matching value
73
+ and reject conflicting known values; if any merged observation is missing the
74
+ result is `None`. Split adjustments scale accrued cash with per-security prices.
75
+
69
76
  ```
70
77
  In [5]: moexapi.Bond(moexapi.get_ticker('SU26238RMFS4'))
71
78
  Out[5]: Bond(secid='SU26238RMFS4', shortname='ОФЗ 26238', amortization=[Amortization(date=datetime.date(2041, 5, 15), value=1000, initialfacevalue=1000)], coupons=[Coupon(date=2021-12-08, value=34.04), Coupon(date=2022-06-08, value=35.4), Coupon(date=2022-12-07, value=35.4), Coupon(date=2023-06-07, value=35.4), Coupon(date=2023-12-06, value=35.4), Coupon(date=2024-06-05, value=35.4), Coupon(date=2024-12-04, value=35.4), Coupon(date=2025-06-04, value=35.4), Coupon(date=2025-12-03, value=35.4), Coupon(date=2026-06-03, value=35.4), Coupon(date=2026-12-02, value=35.4), Coupon(date=2027-06-02, value=35.4), Coupon(date=2027-12-01, value=35.4), Coupon(date=2028-05-31, value=35.4), Coupon(date=2028-11-29, value=35.4), Coupon(date=2029-05-30, value=35.4), Coupon(date=2029-11-28, value=35.4), Coupon(date=2030-05-29, value=35.4), Coupon(date=2030-11-27, value=35.4), Coupon(date=2031-05-28, value=35.4), Coupon(date=2031-11-26, value=35.4), Coupon(date=2032-05-26, value=35.4), Coupon(date=2032-11-24, value=35.4), Coupon(date=2033-05-25, value=35.4), Coupon(date=2033-11-23, value=35.4), Coupon(date=2034-05-24, value=35.4), Coupon(date=2034-11-22, value=35.4), Coupon(date=2035-05-23, value=35.4), Coupon(date=2035-11-21, value=35.4), Coupon(date=2036-05-21, value=35.4), Coupon(date=2036-11-19, value=35.4), Coupon(date=2037-05-20, value=35.4), Coupon(date=2037-11-18, value=35.4), Coupon(date=2038-05-19, value=35.4), Coupon(date=2038-11-17, value=35.4), Coupon(date=2039-05-18, value=35.4), Coupon(date=2039-11-16, value=35.4), Coupon(date=2040-05-16, value=35.4), Coupon(date=2040-11-14, value=35.4), Coupon(date=2041-05-15, value=35.4)], offers=[])
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moexapi"
7
- version = "1.7.20"
7
+ version = "1.7.22"
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