grind-t-moex 0.1.0__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.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.3
2
+ Name: grind-t-moex
3
+ Version: 0.1.0
4
+ Summary: Async Python client for the Moscow Exchange ISS API
5
+ Author: grind-t
6
+ Author-email: grind-t <grindt.gd@gmail.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.13.5
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # grind-t-moex
13
+
14
+ Async Python client for the [Moscow Exchange ISS API](https://iss.moex.com/iss/reference/).
15
+ Python port of [`@grind-t/moex`](https://github.com/grind-t/moex).
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ uv add grind-t-moex
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ import asyncio
27
+
28
+ import httpx
29
+ import moex
30
+
31
+
32
+ async def main():
33
+ bonds = await moex.get_moex_bonds(primary_board=1)
34
+ print(bonds[0]["SECID"], bonds[0]["COUPONPERCENT"])
35
+
36
+ # Reuse one connection pool for several requests
37
+ async with httpx.AsyncClient() as client:
38
+ description = await moex.get_moex_security_description("SU26238RMFS4", client=client)
39
+ amortizations = await moex.get_moex_bond_amortizations("SU26238RMFS4", client=client)
40
+
41
+
42
+ asyncio.run(main())
43
+ ```
44
+
45
+ ## Functions
46
+
47
+ | Function | ISS resource |
48
+ | --- | --- |
49
+ | `get_moex_bonds(primary_board=)` | `engines/stock/markets/bonds/securities` → `securities` |
50
+ | `get_moex_bonds_market_data()` | `engines/stock/markets/bonds/securities` → `marketdata` |
51
+ | `get_moex_bonds_market_yield()` | `engines/stock/markets/bonds/securities` → `marketdata_yields` |
52
+ | `get_moex_bond_coupons(...)` | `statistics/engines/stock/markets/bonds/bondization` → `coupons` |
53
+ | `get_moex_bond_amortizations(id, ...)` | `securities/{id}/bondization` → `amortizations` |
54
+ | `get_moex_bond_securities()` | all traded bonds from `securities`, paginated |
55
+ | `get_moex_securities(...)` | `securities` → `securities` |
56
+ | `get_moex_security_description(id)` | `securities/{id}` → `description` |
57
+ | `get_moex_daily_table()` | `engines/stock` → `dailytable` |
58
+
59
+ Low-level helpers: `moex_fetch(path, block, params)`, `get_moex_resource`, `parse_moex_block`.
60
+
61
+ All functions accept an optional `client: httpx.AsyncClient`. The `from` query
62
+ parameter is passed as `from_`.
@@ -0,0 +1,51 @@
1
+ # grind-t-moex
2
+
3
+ Async Python client for the [Moscow Exchange ISS API](https://iss.moex.com/iss/reference/).
4
+ Python port of [`@grind-t/moex`](https://github.com/grind-t/moex).
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ uv add grind-t-moex
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```python
15
+ import asyncio
16
+
17
+ import httpx
18
+ import moex
19
+
20
+
21
+ async def main():
22
+ bonds = await moex.get_moex_bonds(primary_board=1)
23
+ print(bonds[0]["SECID"], bonds[0]["COUPONPERCENT"])
24
+
25
+ # Reuse one connection pool for several requests
26
+ async with httpx.AsyncClient() as client:
27
+ description = await moex.get_moex_security_description("SU26238RMFS4", client=client)
28
+ amortizations = await moex.get_moex_bond_amortizations("SU26238RMFS4", client=client)
29
+
30
+
31
+ asyncio.run(main())
32
+ ```
33
+
34
+ ## Functions
35
+
36
+ | Function | ISS resource |
37
+ | --- | --- |
38
+ | `get_moex_bonds(primary_board=)` | `engines/stock/markets/bonds/securities` → `securities` |
39
+ | `get_moex_bonds_market_data()` | `engines/stock/markets/bonds/securities` → `marketdata` |
40
+ | `get_moex_bonds_market_yield()` | `engines/stock/markets/bonds/securities` → `marketdata_yields` |
41
+ | `get_moex_bond_coupons(...)` | `statistics/engines/stock/markets/bonds/bondization` → `coupons` |
42
+ | `get_moex_bond_amortizations(id, ...)` | `securities/{id}/bondization` → `amortizations` |
43
+ | `get_moex_bond_securities()` | all traded bonds from `securities`, paginated |
44
+ | `get_moex_securities(...)` | `securities` → `securities` |
45
+ | `get_moex_security_description(id)` | `securities/{id}` → `description` |
46
+ | `get_moex_daily_table()` | `engines/stock` → `dailytable` |
47
+
48
+ Low-level helpers: `moex_fetch(path, block, params)`, `get_moex_resource`, `parse_moex_block`.
49
+
50
+ All functions accept an optional `client: httpx.AsyncClient`. The `from` query
51
+ parameter is passed as `from_`.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "grind-t-moex"
3
+ version = "0.1.0"
4
+ description = "Async Python client for the Moscow Exchange ISS API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "pydantic>=2.13.5",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "grind-t"
14
+ email = "grindt.gd@gmail.com"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.12.9,<0.13.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [tool.uv.build-backend]
21
+ module-name = "moex"
22
+
23
+ [tool.pytest.ini_options]
24
+ asyncio_mode = "auto"
25
+ markers = ["e2e: hits the live MOEX ISS API"]
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=9.1.1",
30
+ "pytest-asyncio>=1.4.0",
31
+ ]
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "grind-t-moex"
3
+ version = "0.1.0"
4
+ description = "Async Python client for the Moscow Exchange ISS API"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "grind-t", email = "grindt.gd@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ "pydantic>=2.13.5",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.9,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "moex"
21
+
22
+ [tool.pytest.ini_options]
23
+ asyncio_mode = "auto"
24
+ markers = ["e2e: hits the live MOEX ISS API"]
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pytest>=9.1.1",
29
+ "pytest-asyncio>=1.4.0",
30
+ ]
@@ -0,0 +1,42 @@
1
+ from .bond_amortizations import MoexBondAmortization, get_moex_bond_amortizations
2
+ from .bond_coupons import MoexBondCoupon, get_moex_bond_coupons
3
+ from .bond_securities import get_moex_bond_securities
4
+ from .bonds import MoexBond, get_moex_bonds
5
+ from .bonds_market_data import MoexBondMarketData, get_moex_bonds_market_data
6
+ from .bonds_market_yield import MoexBondMarketYield, get_moex_bonds_market_yield
7
+ from .core.fetch import moex_fetch, moex_get_json
8
+ from .core.parse_block import parse_moex_block
9
+ from .core.resource import get_moex_resource
10
+ from .core.types import Engine, Market
11
+ from .daily_table import MoexDailyTableRow, get_moex_daily_table
12
+ from .securities import MoexSecurity, get_moex_securities
13
+ from .security_description import (
14
+ MoexSecurityDescription,
15
+ get_moex_security_description,
16
+ )
17
+
18
+ __all__ = [
19
+ "Engine",
20
+ "Market",
21
+ "MoexBond",
22
+ "MoexBondAmortization",
23
+ "MoexBondCoupon",
24
+ "MoexBondMarketData",
25
+ "MoexBondMarketYield",
26
+ "MoexDailyTableRow",
27
+ "MoexSecurity",
28
+ "MoexSecurityDescription",
29
+ "get_moex_bond_amortizations",
30
+ "get_moex_bond_coupons",
31
+ "get_moex_bond_securities",
32
+ "get_moex_bonds",
33
+ "get_moex_bonds_market_data",
34
+ "get_moex_bonds_market_yield",
35
+ "get_moex_daily_table",
36
+ "get_moex_resource",
37
+ "get_moex_securities",
38
+ "get_moex_security_description",
39
+ "moex_fetch",
40
+ "moex_get_json",
41
+ "parse_moex_block",
42
+ ]
@@ -0,0 +1,51 @@
1
+ from datetime import date
2
+ from typing import Literal
3
+
4
+ import httpx
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .core.fetch import moex_fetch
8
+
9
+
10
+ class MoexBondAmortization(BaseModel):
11
+ isin: str = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
12
+ name: str = Field(min_length=1)
13
+ # Not always whole: e.g. gold-linked bonds have a fractional issue volume.
14
+ issuevalue: float = Field(gt=0)
15
+ amortdate: date
16
+ facevalue: float = Field(gt=0)
17
+ initialfacevalue: float = Field(gt=0)
18
+ faceunit: str = Field(pattern=r"^[A-Z]{3}$")
19
+ # May exceed 100 for indexed bonds; 0 for future payments not yet known.
20
+ valueprc: float = Field(ge=0)
21
+ value: float = Field(ge=0)
22
+ # None when the payment amount is not known yet.
23
+ value_rub: float | None = Field(gt=0)
24
+ data_source: Literal["amortization", "maturity"]
25
+ secid: str = Field(min_length=1)
26
+ primary_boardid: str = Field(pattern=r"^[A-Z0-9]{4}$")
27
+
28
+
29
+ async def get_moex_bond_amortizations(
30
+ id: str,
31
+ *,
32
+ lang: Literal["ru", "en"] | None = None,
33
+ from_: str | None = None,
34
+ till: str | None = None,
35
+ limit: int | None = None,
36
+ start: int | None = None,
37
+ client: httpx.AsyncClient | None = None,
38
+ ) -> list[MoexBondAmortization]:
39
+ rows = await moex_fetch(
40
+ f"securities/{id}/bondization.json",
41
+ "amortizations",
42
+ {
43
+ "lang": lang,
44
+ "from": from_,
45
+ "till": till,
46
+ "limit": "unlimited" if limit is None else limit,
47
+ "start": start,
48
+ },
49
+ client,
50
+ )
51
+ return [MoexBondAmortization.model_validate(row) for row in rows]
@@ -0,0 +1,47 @@
1
+ from datetime import date
2
+ from typing import Literal
3
+
4
+ import httpx
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .core.fetch import moex_fetch
8
+
9
+
10
+ class MoexBondCoupon(BaseModel):
11
+ # None for some old (1990s) bonds.
12
+ isin: str | None = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
13
+ name: str = Field(min_length=1)
14
+ # Not always whole (e.g. gold-linked bonds); 0 for some structured bonds.
15
+ issuevalue: float | None = Field(ge=0)
16
+ coupondate: date
17
+ recorddate: date | None
18
+ startdate: date | None
19
+ initialfacevalue: float | None = Field(gt=0)
20
+ # 0 for some fully amortized bonds.
21
+ facevalue: float = Field(ge=0)
22
+ faceunit: str = Field(pattern=r"^[A-Z]{3}$")
23
+ # None when the payment amount is not known yet.
24
+ value: float | None = Field(gt=0)
25
+ # Annualized, so may exceed 100 for short-period structured bonds.
26
+ valueprc: float | None = Field(gt=0)
27
+ value_rub: float | None = Field(gt=0)
28
+ secid: str = Field(min_length=1)
29
+ primary_boardid: str | None = Field(pattern=r"^[A-Z0-9]{4}$")
30
+
31
+
32
+ async def get_moex_bond_coupons(
33
+ *,
34
+ lang: Literal["ru", "en"] | None = None,
35
+ from_: str | None = None,
36
+ till: str | None = None,
37
+ limit: Literal[5, 10, 20, 100] | None = None,
38
+ start: int | None = None,
39
+ client: httpx.AsyncClient | None = None,
40
+ ) -> list[MoexBondCoupon]:
41
+ rows = await moex_fetch(
42
+ "statistics/engines/stock/markets/bonds/bondization.json",
43
+ "coupons",
44
+ {"lang": lang, "from": from_, "till": till, "limit": limit, "start": start},
45
+ client,
46
+ )
47
+ return [MoexBondCoupon.model_validate(row) for row in rows]
@@ -0,0 +1,30 @@
1
+ import httpx
2
+
3
+ from .securities import MoexSecurity, get_moex_securities
4
+
5
+
6
+ async def get_moex_bond_securities(
7
+ *, client: httpx.AsyncClient | None = None
8
+ ) -> list[MoexSecurity]:
9
+ if client is None:
10
+ async with httpx.AsyncClient() as own_client:
11
+ return await get_moex_bond_securities(client=own_client)
12
+
13
+ securities: list[MoexSecurity] = []
14
+ start = 0
15
+
16
+ while True:
17
+ chunk = await get_moex_securities(
18
+ engine="stock",
19
+ market="bonds",
20
+ is_trading=1,
21
+ start=start,
22
+ limit=100,
23
+ client=client,
24
+ )
25
+
26
+ securities.extend(chunk)
27
+ start += len(chunk)
28
+
29
+ if len(chunk) < 100:
30
+ return securities
@@ -0,0 +1,87 @@
1
+ from datetime import date
2
+ from typing import Annotated, Literal
3
+
4
+ import httpx
5
+ from pydantic import BaseModel, BeforeValidator, Field
6
+
7
+ from .core.fetch import moex_fetch
8
+
9
+ # ISS sends "0000-00-00" instead of null for a missing date.
10
+ MaybeDate = Annotated[
11
+ date | None, BeforeValidator(lambda v: None if v == "0000-00-00" else v)
12
+ ]
13
+
14
+
15
+ class MoexBond(BaseModel):
16
+ SECID: str = Field(min_length=1)
17
+ BOARDID: str = Field(pattern=r"^[A-Z0-9]{4}$")
18
+ SHORTNAME: str = Field(min_length=1)
19
+ # None when the bond has not traded yet (or for a while).
20
+ PREVWAPRICE: float | None = Field(gt=0)
21
+ # No bounds: ranges from -100 to six-figure values for distressed bonds.
22
+ YIELDATPREVWAPRICE: float | None
23
+ COUPONVALUE: float = Field(ge=0)
24
+ # None for bonds without a coupon schedule (mostly structured bonds).
25
+ NEXTCOUPON: MaybeDate
26
+ ACCRUEDINT: float = Field(ge=0)
27
+ PREVPRICE: float | None = Field(gt=0)
28
+ LOTSIZE: int = Field(gt=0)
29
+ # Not always whole: amortized or indexed bonds have fractional face values.
30
+ FACEVALUE: float = Field(gt=0)
31
+ BOARDNAME: str = Field(min_length=1)
32
+ STATUS: Literal["A", "N"]
33
+ # None for perpetual bonds and some bonds with a call option.
34
+ MATDATE: MaybeDate
35
+ DECIMALS: int = Field(ge=0)
36
+ # 0 for bonds without periodic coupons.
37
+ COUPONPERIOD: int = Field(ge=0)
38
+ ISSUESIZE: int = Field(gt=0)
39
+ PREVLEGALCLOSEPRICE: float | None = Field(gt=0)
40
+ # None when the bond has no previous trading day.
41
+ PREVDATE: MaybeDate
42
+ SECNAME: str = Field(min_length=1)
43
+ REMARKS: str | None = Field(pattern=r"^[A-Z]+$")
44
+ MARKETCODE: str = Field(pattern=r"^[A-Z]{3,4}$")
45
+ INSTRID: str = Field(pattern=r"^[A-Z]{4}$")
46
+ # Always null in live data.
47
+ SECTORID: None
48
+ MINSTEP: float = Field(gt=0)
49
+ FACEUNIT: str = Field(pattern=r"^[A-Z]{3}$")
50
+ BUYBACKPRICE: float | None = Field(gt=0)
51
+ BUYBACKDATE: MaybeDate
52
+ ISIN: str = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
53
+ LATNAME: str = Field(min_length=1)
54
+ REGNUMBER: str | None = Field(min_length=1)
55
+ CURRENCYID: str = Field(pattern=r"^[A-Z]{3}$")
56
+ ISSUESIZEPLACED: int | None = Field(gt=0)
57
+ LISTLEVEL: Literal[1, 2, 3]
58
+ SECTYPE: str = Field(pattern=r"^[0-9A-Z]$")
59
+ # None when the rate is not known yet (e.g. floaters, structured bonds).
60
+ COUPONPERCENT: float | None = Field(gt=0)
61
+ OFFERDATE: date | None
62
+ SETTLEDATE: date
63
+ LOTVALUE: float = Field(gt=0)
64
+ FACEVALUEONSETTLEDATE: float | None = Field(gt=0)
65
+ CALLOPTIONDATE: date | None
66
+ PUTOPTIONDATE: date | None
67
+ DATEYIELDFROMISSUER: date | None
68
+ BONDTYPE: str | None = Field(min_length=1)
69
+ BONDSUBTYPE: Literal[
70
+ "До погашения", "До оферты (put)", "До оферты (call)", "Бессрочные"
71
+ ]
72
+ COUPON_DETAILS: str | None = Field(min_length=1)
73
+ FACEVALUE_TYPE: Literal["Фиксированный", "Индексированный"]
74
+
75
+
76
+ async def get_moex_bonds(
77
+ *,
78
+ primary_board: Literal[1, 0] | None = None,
79
+ client: httpx.AsyncClient | None = None,
80
+ ) -> list[MoexBond]:
81
+ rows = await moex_fetch(
82
+ "engines/stock/markets/bonds/securities.json",
83
+ "securities",
84
+ {"primary_board": primary_board},
85
+ client,
86
+ )
87
+ return [MoexBond.model_validate(row) for row in rows]
@@ -0,0 +1,97 @@
1
+ from datetime import datetime, time
2
+
3
+ import httpx
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .core.fetch import moex_fetch
7
+
8
+
9
+ class MoexBondMarketData(BaseModel):
10
+ # Not unique: a bond may trade on several boards.
11
+ SECID: str = Field(min_length=1)
12
+ # None when there are no quotes (e.g. outside trading hours).
13
+ BID: float | None = Field(gt=0)
14
+ # Always null in live data.
15
+ BIDDEPTH: None
16
+ OFFER: float | None = Field(gt=0)
17
+ # Always null in live data.
18
+ OFFERDEPTH: None
19
+ # No bounds: quotes may cross during auctions. ISS sends 0 rather than null
20
+ # for this and the other non-nullable numeric fields below when there is no
21
+ # data, even for bonds that did not trade.
22
+ SPREAD: float
23
+ BIDDEPTHT: int | None = Field(ge=0)
24
+ OFFERDEPTHT: int | None = Field(ge=0)
25
+ # None when the bond has not traded today.
26
+ OPEN: float | None = Field(gt=0)
27
+ LOW: float | None = Field(gt=0)
28
+ HIGH: float | None = Field(gt=0)
29
+ LAST: float | None = Field(gt=0)
30
+ LASTCHANGE: float
31
+ LASTCHANGEPRCNT: float
32
+ QTY: int = Field(ge=0)
33
+ # None on boards without regular trading stats (e.g. TQOD).
34
+ VALUE: float | None = Field(ge=0)
35
+ # No bounds: ranges from -100 to five-figure values for distressed bonds.
36
+ YIELD: float | None
37
+ VALUE_USD: float | None = Field(ge=0)
38
+ WAPRICE: float | None = Field(gt=0)
39
+ LASTCNGTOLASTWAPRICE: float
40
+ WAPTOPREVWAPRICEPRCNT: float
41
+ WAPTOPREVWAPRICE: float
42
+ YIELDATWAPRICE: float | None
43
+ YIELDTOPREVYIELD: float
44
+ CLOSEYIELD: float
45
+ CLOSEPRICE: float | None = Field(gt=0)
46
+ MARKETPRICETODAY: float | None = Field(gt=0)
47
+ MARKETPRICE: float | None = Field(gt=0)
48
+ LASTTOPREVPRICE: float
49
+ NUMTRADES: int | None = Field(ge=0)
50
+ VOLTODAY: int | None = Field(ge=0)
51
+ VALTODAY: int | None = Field(ge=0)
52
+ VALTODAY_USD: int | None = Field(ge=0)
53
+ BOARDID: str = Field(pattern=r"^[A-Z0-9]{4}$")
54
+ # Open set of one-letter codes (N, T, B, C, a seen so far).
55
+ TRADINGSTATUS: str = Field(pattern=r"^[A-Za-z]$")
56
+ UPDATETIME: time
57
+ # In days; 0 when not computed.
58
+ DURATION: int = Field(ge=0)
59
+ # Always null in live data.
60
+ NUMBIDS: None
61
+ NUMOFFERS: None
62
+ CHANGE: float | None
63
+ TIME: time
64
+ # Always null in live data.
65
+ HIGHBID: None
66
+ LOWOFFER: None
67
+ PRICEMINUSPREVWAPRICE: float | None
68
+ # Always null in live data.
69
+ LASTBID: None
70
+ LASTOFFER: None
71
+ LCURRENTPRICE: float | None = Field(gt=0)
72
+ LCLOSEPRICE: float | None = Field(gt=0)
73
+ MARKETPRICE2: float | None = Field(gt=0)
74
+ OPENPERIODPRICE: float | None = Field(gt=0)
75
+ SEQNUM: int = Field(gt=0)
76
+ SYSTIME: datetime
77
+ VALTODAY_RUR: int | None = Field(ge=0)
78
+ # Implied rates, set only for floaters and linkers; no bounds.
79
+ IRICPICLOSE: float | None
80
+ BEICLOSE: float | None
81
+ CBRCLOSE: float | None
82
+ YIELDTOOFFER: float | None
83
+ YIELDLASTCOUPON: float | None
84
+ TRADINGSESSION: str | None = Field(min_length=1)
85
+ CALLOPTIONYIELD: float | None
86
+ CALLOPTIONDURATION: float | None = Field(ge=0)
87
+ ZSPREAD: float | None
88
+ ZSPREADATWAPRICE: float | None
89
+
90
+
91
+ async def get_moex_bonds_market_data(
92
+ *, client: httpx.AsyncClient | None = None
93
+ ) -> list[MoexBondMarketData]:
94
+ rows = await moex_fetch(
95
+ "engines/stock/markets/bonds/securities.json", "marketdata", client=client
96
+ )
97
+ return [MoexBondMarketData.model_validate(row) for row in rows]
@@ -0,0 +1,58 @@
1
+ from datetime import date, datetime
2
+ from typing import Literal
3
+
4
+ import httpx
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .core.fetch import moex_fetch
8
+
9
+
10
+ class MoexBondMarketYield(BaseModel):
11
+ # Not unique: a bond may trade on several boards.
12
+ SECID: str = Field(min_length=1)
13
+ BOARDID: str = Field(pattern=r"^[A-Z0-9]{4}$")
14
+ # Price the yields below are computed from; set even for bonds that did not
15
+ # trade today.
16
+ PRICE: float = Field(gt=0)
17
+ # None for bonds without a maturity date (perpetuals).
18
+ YIELDDATE: date | None
19
+ # Timestamp of the zero-coupon yield curve used in the calculation.
20
+ ZCYCMOMENT: datetime
21
+ YIELDDATETYPE: Literal["MATDATE", "OFFER", "MBS"]
22
+ # No bounds: ranges from -100s to eight-figure values for distressed bonds.
23
+ # None when the yield cannot be computed (e.g. perpetuals).
24
+ EFFECTIVEYIELD: float | None
25
+ # In days.
26
+ DURATION: int | None = Field(ge=0)
27
+ # In basis points; no bounds.
28
+ ZSPREADBP: int | None
29
+ GSPREADBP: int | None
30
+ # None when the bond has not traded today.
31
+ WAPRICE: float | None = Field(gt=0)
32
+ EFFECTIVEYIELDWAPRICE: float | None
33
+ DURATIONWAPRICE: int | None = Field(ge=0)
34
+ # Implied rates, set only for floaters and linkers; no bounds.
35
+ IR: float | None
36
+ ICPI: float | None
37
+ BEI: float | None
38
+ # Always null in live data.
39
+ CBR: None
40
+ # Set only when YIELDDATETYPE is OFFER.
41
+ YIELDTOOFFER: float | None
42
+ YIELDLASTCOUPON: float | None
43
+ # Last trade time; 23:59:59 of the last trading day when the bond has not
44
+ # traded today.
45
+ TRADEMOMENT: datetime
46
+ SEQNUM: int = Field(gt=0)
47
+ SYSTIME: datetime
48
+
49
+
50
+ async def get_moex_bonds_market_yield(
51
+ *, client: httpx.AsyncClient | None = None
52
+ ) -> list[MoexBondMarketYield]:
53
+ rows = await moex_fetch(
54
+ "engines/stock/markets/bonds/securities.json",
55
+ "marketdata_yields",
56
+ client=client,
57
+ )
58
+ return [MoexBondMarketYield.model_validate(row) for row in rows]
File without changes
@@ -0,0 +1,34 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from .parse_block import parse_moex_block
6
+ from .resource import Params, get_moex_resource
7
+
8
+
9
+ async def moex_get_json(
10
+ path: str,
11
+ block: str,
12
+ params: Params | None = None,
13
+ client: httpx.AsyncClient | None = None,
14
+ ) -> dict[str, Any]:
15
+ url = get_moex_resource(path, block, params)
16
+
17
+ if client is None:
18
+ async with httpx.AsyncClient() as own_client:
19
+ response = await own_client.get(url)
20
+ else:
21
+ response = await client.get(url)
22
+
23
+ response.raise_for_status()
24
+ return response.json()
25
+
26
+
27
+ async def moex_fetch(
28
+ path: str,
29
+ block: str,
30
+ params: Params | None = None,
31
+ client: httpx.AsyncClient | None = None,
32
+ ) -> list[Any]:
33
+ data = await moex_get_json(path, block, params, client)
34
+ return parse_moex_block(data[block])
@@ -0,0 +1,14 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import Any, TypeVar
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ def parse_moex_block(block: Mapping[str, Sequence[Any]]) -> list[T]:
8
+ columns = block["columns"]
9
+ records = []
10
+ for row in block["data"]:
11
+ records.append(
12
+ {column: row[i] if i < len(row) else None for i, column in enumerate(columns)}
13
+ )
14
+ return records # type: ignore[return-value]
@@ -0,0 +1,17 @@
1
+ from collections.abc import Mapping
2
+
3
+ import httpx
4
+
5
+ BASE_URL = "https://iss.moex.com/iss/"
6
+
7
+ Params = Mapping[str, str | int | None]
8
+
9
+
10
+ def get_moex_resource(path: str, block: str, params: Params | None = None) -> httpx.URL:
11
+ query = {
12
+ key: str(value) for key, value in (params or {}).items() if value is not None
13
+ }
14
+ query["iss.meta"] = "off"
15
+ query["iss.only"] = block
16
+
17
+ return httpx.URL(BASE_URL + path.lstrip("/"), params=query)
@@ -0,0 +1,54 @@
1
+ from typing import Literal
2
+
3
+ # https://iss.moex.com/iss/index.json?iss.only=engines
4
+ Engine = Literal[
5
+ "stock",
6
+ "state",
7
+ "currency",
8
+ "futures",
9
+ "commodity",
10
+ "interventions",
11
+ "offboard",
12
+ "agro",
13
+ "otc",
14
+ "quotes",
15
+ "money",
16
+ ]
17
+
18
+ # https://iss.moex.com/iss/index.json?iss.only=markets
19
+ Market = Literal[
20
+ "repo",
21
+ "deposit",
22
+ "otcindices",
23
+ "index",
24
+ "shares",
25
+ "bonds",
26
+ "ndm",
27
+ "otc",
28
+ "ccp",
29
+ "qnv",
30
+ "mamc",
31
+ "foreignshares",
32
+ "foreignndm",
33
+ "moexboard",
34
+ "gcc",
35
+ "credit",
36
+ "selt",
37
+ "futures",
38
+ "main",
39
+ "forts",
40
+ "options",
41
+ "fortsiqs",
42
+ "optionsiqs",
43
+ "sharesndm",
44
+ "nonresndm",
45
+ "nonresrepo",
46
+ "nonresccp",
47
+ "grain",
48
+ "sugar",
49
+ "auctions",
50
+ "buyauctions",
51
+ "saleauctions",
52
+ "standard",
53
+ "classica",
54
+ ]
@@ -0,0 +1,24 @@
1
+ # Imported as a module: the `date` field would otherwise shadow the type.
2
+ import datetime as dt
3
+
4
+ import httpx
5
+ from pydantic import BaseModel
6
+
7
+ from .core.fetch import moex_fetch
8
+
9
+
10
+ # Exceptions to the regular calendar: weekday holidays and working weekends.
11
+ class MoexDailyTableRow(BaseModel):
12
+ date: dt.date
13
+ is_work_day: bool
14
+ # None (or 00:00:00 for both) on some non-working days, so start < stop
15
+ # does not always hold.
16
+ start_time: dt.time | None
17
+ stop_time: dt.time | None
18
+
19
+
20
+ async def get_moex_daily_table(
21
+ *, client: httpx.AsyncClient | None = None
22
+ ) -> list[MoexDailyTableRow]:
23
+ rows = await moex_fetch("engines/stock.json", "dailytable", client=client)
24
+ return [MoexDailyTableRow.model_validate(row) for row in rows]
File without changes
@@ -0,0 +1,75 @@
1
+ from typing import Annotated, Literal
2
+
3
+ import httpx
4
+ from pydantic import BaseModel, BeforeValidator, Field
5
+
6
+ from .core.fetch import moex_fetch
7
+ from .core.types import Engine, Market
8
+
9
+ # ISS sends "" (and for isin even "None") instead of null for some indices,
10
+ # currencies and old state bonds.
11
+ MaybeStr = Annotated[
12
+ str | None, BeforeValidator(lambda v: None if v in ("", "None") else v)
13
+ ]
14
+
15
+
16
+ # Issuer fields (emitent_*), isin and regnumber are None for derivatives,
17
+ # indices, currencies and commodities, and for some foreign securities.
18
+ class MoexSecurity(BaseModel):
19
+ # \w: an old commodity future has a Cyrillic letter ("R1Н4").
20
+ secid: str = Field(pattern=r"^[\w.+-]+$")
21
+ # None for some delisted securities and futures spreads.
22
+ shortname: str | None = Field(min_length=1)
23
+ regnumber: MaybeStr = Field(min_length=1)
24
+ name: str | None = Field(min_length=1)
25
+ # The check digit is missing for a few bond indices.
26
+ isin: MaybeStr = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]?$")
27
+ # None for some futures spreads.
28
+ is_traded: bool | None
29
+ emitent_id: int | None = Field(gt=0)
30
+ emitent_title: str | None = Field(min_length=1)
31
+ # Not always a real INN: foreign issuers have short registry numbers.
32
+ emitent_inn: str | None = Field(pattern=r"^[0-9]{1,12}$")
33
+ # Leading zeros are sometimes dropped.
34
+ emitent_okpo: str | None = Field(pattern=r"^[0-9]{5,8}$")
35
+ # Open sets, e.g. "common_share" in "stock_shares", "option" in
36
+ # "futures_options".
37
+ type: str = Field(pattern=r"^[a-z]+(_[a-z]+)*$")
38
+ group: str = Field(pattern=r"^[a-z]+(_[a-z]+)*$")
39
+ # None for some delisted securities (mostly bonds); 3 letters for a few old
40
+ # boards ("FOB", "BKT").
41
+ primary_boardid: str | None = Field(pattern=r"^[A-Z0-9]{3,4}$")
42
+ # Only set for some stock market securities (shares, bonds, funds).
43
+ marketprice_boardid: str | None = Field(pattern=r"^[A-Z0-9]{4}$")
44
+
45
+
46
+ async def get_moex_securities(
47
+ *,
48
+ q: str | None = None,
49
+ lang: Literal["ru", "en"] | None = None,
50
+ engine: Engine | None = None,
51
+ is_trading: Literal[1, 0] | None = None,
52
+ market: Market | None = None,
53
+ group_by: Literal["group", "type"] | None = None,
54
+ limit: Literal[5, 10, 20, 100] | None = None,
55
+ group_by_filter: str | None = None,
56
+ start: int | None = None,
57
+ client: httpx.AsyncClient | None = None,
58
+ ) -> list[MoexSecurity]:
59
+ rows = await moex_fetch(
60
+ "securities.json",
61
+ "securities",
62
+ {
63
+ "q": q,
64
+ "lang": lang,
65
+ "engine": engine,
66
+ "is_trading": is_trading,
67
+ "market": market,
68
+ "group_by": group_by,
69
+ "limit": limit,
70
+ "group_by_filter": group_by_filter,
71
+ "start": start,
72
+ },
73
+ client,
74
+ )
75
+ return [MoexSecurity.model_validate(row) for row in rows]
@@ -0,0 +1,167 @@
1
+ from datetime import date, time
2
+ from typing import Literal
3
+
4
+ import httpx
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .core.fetch import moex_get_json
8
+
9
+
10
+ # Keys vary by security type; only TYPE, TYPENAME, GROUP and GROUPNAME are
11
+ # present for every security. Absent keys are None.
12
+ class MoexSecurityDescription(BaseModel):
13
+ # Absent for currency fixings and OTC currency indices.
14
+ SECID: str | None = Field(default=None, min_length=1)
15
+ # Absent for old commodity futures and some indices.
16
+ SHORTNAME: str | None = Field(default=None, min_length=1)
17
+ NAME: str | None = Field(default=None, min_length=1)
18
+ LATSHORTNAME: str | None = Field(default=None, min_length=1)
19
+ LATNAME: str | None = Field(default=None, min_length=1)
20
+ CALCMODE: str | None = Field(default=None, pattern=r"^[A-Z0-9-]+$")
21
+ CALCMODEDESCR: str | None = Field(default=None, min_length=1)
22
+ CURRENCYID: str | None = Field(default=None, pattern=r"^[A-Z]{3}$")
23
+ FREQUENCY: str | None = Field(default=None, min_length=1)
24
+ SCHEDULE: str | None = Field(default=None, min_length=1)
25
+ DECIMALS: int | None = Field(default=None, ge=0)
26
+ INITIALVALUE: float | None = Field(default=None, gt=0)
27
+ ISSUEDATE: date | None = None
28
+ TRADINGSESSION: str | None = Field(default=None, min_length=1)
29
+ # iNAV indices mark the fund with a leading or trailing underscore.
30
+ UNDERLYINGTICKER: str | None = Field(default=None, pattern=r"^_?[A-Z0-9]+_?$")
31
+ UNDERLYINGISIN: str | None = Field(
32
+ default=None, pattern=r"^_?[A-Z]{2}[A-Z0-9]{9}[0-9]_?$"
33
+ )
34
+ REBALANCE: str | None = Field(default=None, min_length=1)
35
+ WEIGHTLIMITTYPE: str | None = Field(default=None, min_length=1)
36
+ TYPENAME: str = Field(min_length=1)
37
+ GROUP: str = Field(pattern=r"^[a-z_]+$")
38
+ TYPE: str = Field(pattern=r"^[a-z_]+$")
39
+ GROUPNAME: str = Field(min_length=1)
40
+ FIRSTCALCDATE: date | None = None
41
+ # A count, but "переменное" (variable) for some indices.
42
+ CONSTITUENTS: str | None = Field(default=None, min_length=1)
43
+ DESCRIPTION: str | None = Field(default=None, min_length=1)
44
+ INITIALCAPITALIZATION: float | None = Field(default=None, gt=0)
45
+ INITIALD: float | None = Field(default=None, gt=0)
46
+ ISSUENAME: str | None = Field(default=None, min_length=1)
47
+ # One index has a truncated 11-character ISIN (check digit missing).
48
+ ISIN: str | None = Field(default=None, pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]?$")
49
+ # Not always whole: down to 1e-05 for some shares.
50
+ FACEVALUE: float | None = Field(default=None, gt=0)
51
+ # Includes non-ISO codes such as SUR, GLD and BKT.
52
+ FACEUNIT: str | None = Field(default=None, pattern=r"^[A-Z]{3}$")
53
+ HASPROSPECTUS: bool | None = None
54
+ HASDEFAULT: bool | None = None
55
+ HASTECHNICALDEFAULT: bool | None = None
56
+ # A 0/1 flag, though ISS declares it a number.
57
+ EMITENTMISMATCHCUR: int | None = Field(default=None, ge=0, le=1)
58
+ ISQUALIFIEDINVESTORS: bool | None = None
59
+ QUALINVESTORGROUP: str | None = Field(default=None, min_length=1)
60
+ EMITTER_ID: int | None = Field(default=None, gt=0)
61
+ REGNUMBER: str | None = Field(default=None, min_length=1)
62
+ ISSUESIZE: int | None = Field(default=None, gt=0)
63
+ REGISTRY_DATE: date | None = None
64
+ DECISIONDATE: date | None = None
65
+ LISTLEVEL: int | None = Field(default=None, ge=1, le=3)
66
+ MORNINGSESSION: bool | None = None
67
+ EVENINGSESSION: bool | None = None
68
+ WEEKENDSESSION: bool | None = None
69
+ MATDATE: date | None = None
70
+ INITIALFACEVALUE: float | None = Field(default=None, gt=0)
71
+ STARTDATEMOEX: date | None = None
72
+ ISCONCESSIONAGREEMENT: bool | None = None
73
+ INCLUDEDBYMOEX: bool | None = None
74
+ DAYSTOREDEMPTION: int | None = Field(default=None, ge=0)
75
+ COUPONFREQUENCY: int | None = Field(default=None, gt=0)
76
+ COUPONDATE: date | None = None
77
+ # 0 for some structured bonds.
78
+ COUPONPERCENT: float | None = Field(default=None, ge=0)
79
+ COUPONVALUE: float | None = Field(default=None, ge=0)
80
+ BOND_TYPE: str | None = Field(default=None, min_length=1)
81
+ BOND_SUBTYPE: (
82
+ Literal["До погашения", "До оферты (put)", "До оферты (call)", "Бессрочные"]
83
+ | None
84
+ ) = None
85
+ EARLYREPAYMENT: bool | None = None
86
+ COUPON_BENCHMARK: str | None = Field(default=None, pattern=r"^[A-Z]+$")
87
+ # ISS declares it a string, e.g. "1.30".
88
+ COUPON_BENCHMARK_SPREAD: str | None = Field(
89
+ default=None, pattern=r"^-?[0-9]+\.[0-9]+$"
90
+ )
91
+ AMORTBOND: bool | None = None
92
+ BUYBACKDATE: date | None = None
93
+ EMITTER_VALUE_BUILDING: bool | None = None
94
+ LOTSIZE: int | None = Field(default=None, gt=0)
95
+ TIMETABLE: str | None = Field(default=None, min_length=1)
96
+ DELIVERYTYPE: str | None = Field(default=None, min_length=1)
97
+ FRSTTRADE: date | None = None
98
+ LSTTRADE: date | None = None
99
+ LSTDELDATE: date | None = None
100
+ ASSETCODE: str | None = Field(default=None, pattern=r"^[A-Za-z0-9]+$")
101
+ EXECTYPE: str | None = Field(default=None, min_length=1)
102
+ CONTRACTNAME: str | None = Field(default=None, min_length=1)
103
+ GROUPTYPE: str | None = Field(default=None, min_length=1)
104
+ UNIT: str | None = Field(default=None, min_length=1)
105
+ EXPIRATION_TYPE: str | None = Field(default=None, min_length=1)
106
+ EXPIRATION_TIME: time | None = None
107
+ SETTLETYPE: Literal["Расчетный", "Поставочный"] | None = None
108
+ OPTIONTYPE: Literal["C", "P"] | None = None
109
+ STRIKE: float | None = Field(default=None, gt=0)
110
+ MARGINSTYLE: Literal["Премиальный", "Маржируемый"] | None = None
111
+ UNDERLYINGASSET: str | None = Field(default=None, min_length=1)
112
+ SERIES_NAME: str | None = Field(default=None, min_length=1)
113
+ DEPOSITARY_ORG_NAME: str | None = Field(default=None, min_length=1)
114
+ # Not always whole: e.g. 0.2 shares per receipt.
115
+ SHARESPERRECEIPT: float | None = Field(default=None, gt=0)
116
+ HIGHRISK: bool | None = None
117
+ SECSUBTYPE: str | None = Field(default=None, min_length=1)
118
+ SUBORDBOND: bool | None = None
119
+ PROGRAMREGISTRYNUMBER: str | None = Field(default=None, min_length=1)
120
+ STRUCTBOND: bool | None = None
121
+ ASSET: str | None = Field(default=None, min_length=1)
122
+ PARENTINDEX: str | None = Field(default=None, min_length=1)
123
+ ISPARENT: bool | None = None
124
+ BBG: str | None = Field(default=None, min_length=1)
125
+ RIC: str | None = Field(default=None, min_length=1)
126
+ VOLUME_UNIT: str | None = Field(default=None, min_length=1)
127
+ DEPOSITARYNOTETYPE: str | None = Field(default=None, min_length=1)
128
+ ISSUESIZEPLANNED: int | None = Field(default=None, gt=0)
129
+ PERPETUAL_FUTURES: bool | None = None
130
+ FOSECTYPE: str | None = Field(default=None, pattern=r"^[A-Z0-9]+$")
131
+ # The fields below come only with old commodity futures.
132
+ FACEUNITNAME: str | None = Field(default=None, min_length=1)
133
+ DELIVERYMINQTY: str | None = Field(default=None, min_length=1)
134
+ PRICEFULLNAME: str | None = Field(default=None, min_length=1)
135
+ MINPRICEFLUCT: str | None = Field(default=None, min_length=1)
136
+ DURATION: int | None = Field(default=None, gt=0)
137
+ DELCOMMISSION: str | None = Field(default=None, min_length=1)
138
+ COMMISSIONRATE: float | None = Field(default=None, gt=0)
139
+ FINALPRICEMETHOD: str | None = Field(default=None, min_length=1)
140
+ STARTDELDATE: date | None = None
141
+ PRICEMVTLIMITEXTNAME: str | None = Field(default=None, min_length=1)
142
+ SHARELIMIT: float | None = Field(default=None, gt=0, le=100)
143
+ THRESHOLD: int | None = Field(default=None, gt=0)
144
+ MAXORDVOL: int | None = Field(default=None, gt=0)
145
+ DMRATEEXTNAME: str | None = Field(default=None, min_length=1)
146
+ TIMECOURSEINT: time | None = None
147
+ TIMECOURSECLR: time | None = None
148
+ # Only for the bi-currency basket.
149
+ BASKET_DESCR1: str | None = Field(default=None, min_length=1)
150
+ BASKET_DESCR2: str | None = Field(default=None, min_length=1)
151
+ BASKET_DESCR3: str | None = Field(default=None, min_length=1)
152
+ BASKET_DESCR4: str | None = Field(default=None, min_length=1)
153
+ BASKET_DESCR5: str | None = Field(default=None, min_length=1)
154
+
155
+
156
+ async def get_moex_security_description(
157
+ id: str, *, client: httpx.AsyncClient | None = None
158
+ ) -> MoexSecurityDescription:
159
+ data = await moex_get_json(f"securities/{id}.json", "description", client=client)
160
+ description = data["description"]
161
+ columns = description["columns"]
162
+ name_index = columns.index("name")
163
+ value_index = columns.index("value")
164
+
165
+ # Values are strings; the model converts them by field type.
166
+ result = {row[name_index]: row[value_index] for row in description["data"]}
167
+ return MoexSecurityDescription.model_validate(result)