algierex 1.0.0__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.
- algierex/__init__.py +33 -0
- algierex/_client.py +167 -0
- algierex/_core.py +183 -0
- algierex/_types.py +136 -0
- algierex/py.typed +0 -0
- algierex-1.0.0.dist-info/METADATA +109 -0
- algierex-1.0.0.dist-info/RECORD +10 -0
- algierex-1.0.0.dist-info/WHEEL +5 -0
- algierex-1.0.0.dist-info/licenses/LICENSE +21 -0
- algierex-1.0.0.dist-info/top_level.txt +1 -0
algierex/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Python SDK for the Algierex market data API: the Algerian exchange's
|
|
2
|
+
prices, sessions, Dzair index, OAT yield curve, exchange rates, company
|
|
3
|
+
events and news.
|
|
4
|
+
|
|
5
|
+
from algierex import Algierex
|
|
6
|
+
client = Algierex() # reads ALGIEREX_API_KEY
|
|
7
|
+
prices = client.get_prices("SAI", from_="2026-01-01")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._client import Algierex
|
|
11
|
+
from ._core import (
|
|
12
|
+
AlgierexError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
BadRequestError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PaymentRequiredError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
to_frame,
|
|
19
|
+
)
|
|
20
|
+
from ._types import * # noqa: F401,F403 (response TypedDicts)
|
|
21
|
+
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Algierex",
|
|
26
|
+
"AlgierexError",
|
|
27
|
+
"AuthenticationError",
|
|
28
|
+
"BadRequestError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"PaymentRequiredError",
|
|
31
|
+
"RateLimitError",
|
|
32
|
+
"to_frame",
|
|
33
|
+
]
|
algierex/_client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""The Algierex API client. Generated by sdk/generate.mjs from the OpenAPI spec. Do not edit; run `npm run sdk`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from typing import Iterator, List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
from ._core import _Core
|
|
9
|
+
from ._types import CalendarEvent, FxLatest, FxRow, IndexDay, NewsItem, PriceDay, Session, Ticker, YieldCurve
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _quote(value: str) -> str:
|
|
13
|
+
return urllib.parse.quote(str(value), safe="")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Algierex(_Core):
|
|
17
|
+
"""Client for the Algierex market data API (https://algierex.com/api/v1).
|
|
18
|
+
|
|
19
|
+
client = Algierex() # reads ALGIEREX_API_KEY
|
|
20
|
+
prices = client.get_prices("SAI", from_="2026-01-01")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def list_tickers(self) -> List[Ticker]:
|
|
24
|
+
"""List securities.
|
|
25
|
+
|
|
26
|
+
Every listed security with its names in English, French and Arabic,
|
|
27
|
+
sector, ISIN and capital facts.
|
|
28
|
+
"""
|
|
29
|
+
return self._data("/tickers")
|
|
30
|
+
|
|
31
|
+
def get_prices(self, ticker: str, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[PriceDay]:
|
|
32
|
+
"""Daily prices for one security.
|
|
33
|
+
|
|
34
|
+
One row per trading day, oldest first, across every market the security
|
|
35
|
+
has traded in. Several trade lines in one session (common for OATs) are
|
|
36
|
+
merged into one day: volumes add up and the last price is the close.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
ticker: Symbol, for example SAI. Not case sensitive.
|
|
40
|
+
from_: First date to include (YYYY-MM-DD).
|
|
41
|
+
to: Last date to include (YYYY-MM-DD).
|
|
42
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
43
|
+
"""
|
|
44
|
+
return list(self.iter_prices(ticker, from_=from_, to=to, page_size=page_size))
|
|
45
|
+
|
|
46
|
+
def iter_prices(self, ticker: str, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[PriceDay]:
|
|
47
|
+
"""Like get_prices, but yields rows as pages arrive, for long histories."""
|
|
48
|
+
return self._pages(f"/prices/{_quote(ticker)}", {"from": from_, "to": to}, page_size)
|
|
49
|
+
|
|
50
|
+
def list_sessions(self, *, market: Optional[Literal["main", "bond", "sme", "oat"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[Session]:
|
|
51
|
+
"""Market sessions.
|
|
52
|
+
|
|
53
|
+
One market's sessions with their totals, oldest first. Premium and PI
|
|
54
|
+
securities trade in main market sessions.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
market: main (equities), sme (growth market), bond (corporate bonds) or oat (Treasury bonds).
|
|
58
|
+
from_: First date to include (YYYY-MM-DD).
|
|
59
|
+
to: Last date to include (YYYY-MM-DD).
|
|
60
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
61
|
+
"""
|
|
62
|
+
return list(self.iter_sessions(market=market, from_=from_, to=to, page_size=page_size))
|
|
63
|
+
|
|
64
|
+
def iter_sessions(self, *, market: Optional[Literal["main", "bond", "sme", "oat"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[Session]:
|
|
65
|
+
"""Like list_sessions, but yields rows as pages arrive, for long histories."""
|
|
66
|
+
return self._pages("/sessions", {"market": market, "from": from_, "to": to}, page_size)
|
|
67
|
+
|
|
68
|
+
def get_dzair_index(self, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[IndexDay]:
|
|
69
|
+
"""Dzair index.
|
|
70
|
+
|
|
71
|
+
The Dzair index close for each session date, oldest first.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
from_: First date to include (YYYY-MM-DD).
|
|
75
|
+
to: Last date to include (YYYY-MM-DD).
|
|
76
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
77
|
+
"""
|
|
78
|
+
return list(self.iter_dzair_index(from_=from_, to=to, page_size=page_size))
|
|
79
|
+
|
|
80
|
+
def iter_dzair_index(self, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[IndexDay]:
|
|
81
|
+
"""Like get_dzair_index, but yields rows as pages arrive, for long histories."""
|
|
82
|
+
return self._pages("/index/dzair", {"from": from_, "to": to}, page_size)
|
|
83
|
+
|
|
84
|
+
def get_yield_curve(self, *, date: Optional[str] = None) -> YieldCurve:
|
|
85
|
+
"""OAT yield curve.
|
|
86
|
+
|
|
87
|
+
A Nelson-Siegel curve fitted to Algerian Treasury bonds (OATs) that
|
|
88
|
+
actually traded in the last 60 OAT sessions, with the bonds behind it.
|
|
89
|
+
Each bond counts at its most recent trade in that window, so a point can
|
|
90
|
+
be up to 60 sessions old. `fit` is null when fewer than five bonds
|
|
91
|
+
traded.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
date: The curve as of the last OAT session on or before this date. Omit for the latest.
|
|
95
|
+
"""
|
|
96
|
+
return self._data("/yield-curve", {"date": date})
|
|
97
|
+
|
|
98
|
+
def list_fx(self) -> List[FxLatest]:
|
|
99
|
+
"""Latest exchange rates.
|
|
100
|
+
|
|
101
|
+
The latest rate per currency and source, in dinars per unit. `official`
|
|
102
|
+
is the Bank of Algeria rate; `exchangedz` is the parallel market.
|
|
103
|
+
"""
|
|
104
|
+
return self._data("/fx")
|
|
105
|
+
|
|
106
|
+
def get_fx_history(self, code: str, *, source: Optional[Literal["official", "exchangedz"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[FxRow]:
|
|
107
|
+
"""Exchange rate history.
|
|
108
|
+
|
|
109
|
+
One currency's daily rates from one source, oldest first, in dinars per
|
|
110
|
+
unit. Official rates the upstream feed sent inverted are left out.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code: Currency code, for example USD or EUR.
|
|
114
|
+
source: official (Bank of Algeria) or exchangedz (parallel market).
|
|
115
|
+
from_: First date to include (YYYY-MM-DD).
|
|
116
|
+
to: Last date to include (YYYY-MM-DD).
|
|
117
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
118
|
+
"""
|
|
119
|
+
return list(self.iter_fx_history(code, source=source, from_=from_, to=to, page_size=page_size))
|
|
120
|
+
|
|
121
|
+
def iter_fx_history(self, code: str, *, source: Optional[Literal["official", "exchangedz"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[FxRow]:
|
|
122
|
+
"""Like get_fx_history, but yields rows as pages arrive, for long histories."""
|
|
123
|
+
return self._pages(f"/fx/{_quote(code)}", {"source": source, "from": from_, "to": to}, page_size)
|
|
124
|
+
|
|
125
|
+
def get_calendar(self, *, from_: Optional[str] = None, to: Optional[str] = None, ticker: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, page_size: Optional[int] = None) -> List[CalendarEvent]:
|
|
126
|
+
"""Company events calendar.
|
|
127
|
+
|
|
128
|
+
General meetings, dividend and coupon payments, subscription periods and
|
|
129
|
+
listings, past and upcoming, ordered by date. The same calendar the
|
|
130
|
+
website shows: dates taken from news items, merged with a curated
|
|
131
|
+
calendar, without duplicates.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
from_: First date to include (YYYY-MM-DD).
|
|
135
|
+
to: Last date to include (YYYY-MM-DD).
|
|
136
|
+
ticker: Only events for this security. Not case sensitive.
|
|
137
|
+
locale: Language of labels and titles: en, fr or ar. Text missing in that language falls back to the French original.
|
|
138
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
139
|
+
"""
|
|
140
|
+
return list(self.iter_calendar(from_=from_, to=to, ticker=ticker, locale=locale, page_size=page_size))
|
|
141
|
+
|
|
142
|
+
def iter_calendar(self, *, from_: Optional[str] = None, to: Optional[str] = None, ticker: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, page_size: Optional[int] = None) -> Iterator[CalendarEvent]:
|
|
143
|
+
"""Like get_calendar, but yields rows as pages arrive, for long histories."""
|
|
144
|
+
return self._pages("/calendar", {"from": from_, "to": to, "ticker": ticker, "locale": locale}, page_size)
|
|
145
|
+
|
|
146
|
+
def list_news(self, *, ticker: Optional[str] = None, source: Optional[Literal["cosob-visa", "cosob-decision", "cosob-communique", "sgbv-avis", "sgbv-press", "bank-of-algeria", "issuer-ir", "editorial"]] = None, from_: Optional[str] = None, to: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, include_routine: Optional[bool] = None, page_size: Optional[int] = None) -> List[NewsItem]:
|
|
147
|
+
"""News.
|
|
148
|
+
|
|
149
|
+
News from COSOB, the exchange, the Bank of Algeria and issuers, newest
|
|
150
|
+
first, with a summary in the asked language and a link to the full
|
|
151
|
+
article on Algierex. Routine trading-parameter notices are left out
|
|
152
|
+
unless `includeRoutine=true`.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
ticker: Only news about this security. Not case sensitive.
|
|
156
|
+
source: Only news from one origin.
|
|
157
|
+
from_: First publication day to include (YYYY-MM-DD, Algiers time).
|
|
158
|
+
to: Last publication day to include (YYYY-MM-DD, Algiers time).
|
|
159
|
+
locale: Language of labels and titles: en, fr or ar. Text missing in that language falls back to the French original.
|
|
160
|
+
include_routine: Also return routine trading-parameter notices.
|
|
161
|
+
page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
|
|
162
|
+
"""
|
|
163
|
+
return list(self.iter_news(ticker=ticker, source=source, from_=from_, to=to, locale=locale, include_routine=include_routine, page_size=page_size))
|
|
164
|
+
|
|
165
|
+
def iter_news(self, *, ticker: Optional[str] = None, source: Optional[Literal["cosob-visa", "cosob-decision", "cosob-communique", "sgbv-avis", "sgbv-press", "bank-of-algeria", "issuer-ir", "editorial"]] = None, from_: Optional[str] = None, to: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, include_routine: Optional[bool] = None, page_size: Optional[int] = None) -> Iterator[NewsItem]:
|
|
166
|
+
"""Like list_news, but yields rows as pages arrive, for long histories."""
|
|
167
|
+
return self._pages("/news", {"ticker": ticker, "source": source, "from": from_, "to": to, "locale": locale, "includeRoutine": include_routine}, page_size)
|
algierex/_core.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""HTTP core of the Algierex SDK: requests, errors, retries, paging.
|
|
2
|
+
|
|
3
|
+
Hand-written. The API methods live in _client.py, generated from the
|
|
4
|
+
OpenAPI spec by sdk/generate.mjs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import urllib.request
|
|
15
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://algierex.com/api/v1"
|
|
18
|
+
# The API's largest page: fewer calls against the plan's limits.
|
|
19
|
+
DEFAULT_PAGE_SIZE = 1000
|
|
20
|
+
USER_AGENT = "algierex-python"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AlgierexError(Exception):
|
|
24
|
+
"""An error answered by the API, or a failure to reach it."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str, status: Optional[int] = None, code: Optional[str] = None):
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status = status
|
|
29
|
+
self.code = code
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BadRequestError(AlgierexError):
|
|
33
|
+
"""400: a parameter is not valid."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(AlgierexError):
|
|
37
|
+
"""401: the key is missing, wrong, or revoked."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PaymentRequiredError(AlgierexError):
|
|
41
|
+
"""402: the key's account has no active plan, or its last payment failed."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotFoundError(AlgierexError):
|
|
45
|
+
"""404: no such security, currency, route, or date."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitError(AlgierexError):
|
|
49
|
+
"""429: the plan's limit is used up. retry_after is in seconds."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, message: str, retry_after: float, status: int = 429, code: str = "rate_limited"):
|
|
52
|
+
super().__init__(message, status, code)
|
|
53
|
+
self.retry_after = retry_after
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_ERRORS = {400: BadRequestError, 401: AuthenticationError, 402: PaymentRequiredError, 404: NotFoundError}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _Core:
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
api_key: Optional[str] = None,
|
|
63
|
+
*,
|
|
64
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
65
|
+
timeout: float = 30.0,
|
|
66
|
+
max_retries: int = 3,
|
|
67
|
+
max_retry_wait: float = 60.0,
|
|
68
|
+
_sleep: Callable[[float], None] = time.sleep,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Create a client.
|
|
71
|
+
|
|
72
|
+
api_key: your key (alx_...). Defaults to the ALGIEREX_API_KEY
|
|
73
|
+
environment variable.
|
|
74
|
+
max_retries: how many times to wait and retry when a rate limit is
|
|
75
|
+
hit (429). 0 raises RateLimitError at once.
|
|
76
|
+
max_retry_wait: the longest single wait, in seconds; a longer
|
|
77
|
+
Retry-After raises RateLimitError instead of sleeping.
|
|
78
|
+
"""
|
|
79
|
+
key = api_key or os.environ.get("ALGIEREX_API_KEY")
|
|
80
|
+
if not key:
|
|
81
|
+
raise AuthenticationError("No API key. Pass api_key or set ALGIEREX_API_KEY.")
|
|
82
|
+
self._key = key
|
|
83
|
+
self._base_url = base_url.rstrip("/")
|
|
84
|
+
self._timeout = timeout
|
|
85
|
+
self._max_retries = max_retries
|
|
86
|
+
self._max_retry_wait = max_retry_wait
|
|
87
|
+
self._sleep = _sleep
|
|
88
|
+
|
|
89
|
+
def _request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
90
|
+
query = {k: _param(v) for k, v in (params or {}).items() if v is not None}
|
|
91
|
+
url = self._base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
|
|
92
|
+
attempt = 0
|
|
93
|
+
while True:
|
|
94
|
+
req = urllib.request.Request(
|
|
95
|
+
url,
|
|
96
|
+
headers={
|
|
97
|
+
"Authorization": f"Bearer {self._key}",
|
|
98
|
+
"Accept": "application/json",
|
|
99
|
+
"User-Agent": USER_AGENT,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as res:
|
|
104
|
+
raw = res.read()
|
|
105
|
+
except urllib.error.HTTPError as err:
|
|
106
|
+
status = err.code
|
|
107
|
+
with err:
|
|
108
|
+
body = _json_or_none(err.read())
|
|
109
|
+
error = body.get("error") if isinstance(body, dict) else None
|
|
110
|
+
error = error if isinstance(error, dict) else {}
|
|
111
|
+
message = error.get("message") or f"HTTP {status}"
|
|
112
|
+
code = error.get("code")
|
|
113
|
+
if status == 429:
|
|
114
|
+
retry_after = _seconds(err.headers.get("Retry-After"))
|
|
115
|
+
if attempt < self._max_retries and retry_after <= self._max_retry_wait:
|
|
116
|
+
attempt += 1
|
|
117
|
+
self._sleep(retry_after)
|
|
118
|
+
continue
|
|
119
|
+
raise RateLimitError(message, retry_after) from None
|
|
120
|
+
raise _ERRORS.get(status, AlgierexError)(message, status, code) from None
|
|
121
|
+
except urllib.error.URLError as err:
|
|
122
|
+
raise AlgierexError(f"Could not reach {self._base_url}: {err.reason}") from None
|
|
123
|
+
except OSError as err: # timeouts and dropped connections
|
|
124
|
+
raise AlgierexError(f"Could not reach {self._base_url}: {err}") from None
|
|
125
|
+
body = _json_or_none(raw)
|
|
126
|
+
if body is None:
|
|
127
|
+
raise AlgierexError(f"The API answered with something that is not JSON ({url}).")
|
|
128
|
+
return body
|
|
129
|
+
|
|
130
|
+
def _data(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
131
|
+
return self._request(path, params)["data"]
|
|
132
|
+
|
|
133
|
+
def _pages(self, path: str, params: Dict[str, Any], page_size: Optional[int]) -> Iterator[Any]:
|
|
134
|
+
"""Every row of a paged route, following nextCursor to the end."""
|
|
135
|
+
params = dict(params, limit=page_size or DEFAULT_PAGE_SIZE)
|
|
136
|
+
seen = set()
|
|
137
|
+
while True:
|
|
138
|
+
body = self._request(path, params)
|
|
139
|
+
yield from body["data"]
|
|
140
|
+
cursor = body.get("nextCursor")
|
|
141
|
+
if not cursor:
|
|
142
|
+
return
|
|
143
|
+
if cursor in seen:
|
|
144
|
+
raise AlgierexError("The API returned the same page cursor twice; stopping to avoid a loop.")
|
|
145
|
+
seen.add(cursor)
|
|
146
|
+
params["cursor"] = cursor
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _param(value: Any) -> str:
|
|
150
|
+
if isinstance(value, bool):
|
|
151
|
+
return "true" if value else "false"
|
|
152
|
+
return str(value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _seconds(value: Optional[str]) -> float:
|
|
156
|
+
"""Retry-After in seconds; 1 when missing or not a number."""
|
|
157
|
+
try:
|
|
158
|
+
return max(0.0, float(value)) if value else 1.0
|
|
159
|
+
except ValueError:
|
|
160
|
+
return 1.0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _json_or_none(raw: bytes) -> Any:
|
|
164
|
+
try:
|
|
165
|
+
return json.loads(raw.decode("utf-8"))
|
|
166
|
+
except (ValueError, UnicodeDecodeError):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def to_frame(rows: List[Dict[str, Any]], index: Optional[str] = "date"):
|
|
171
|
+
"""Turn rows into a pandas DataFrame, indexed by date when there is one.
|
|
172
|
+
|
|
173
|
+
Needs pandas (pip install "algierex[pandas]").
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
import pandas as pd # type: ignore[import-not-found]
|
|
177
|
+
except ImportError as err: # pragma: no cover
|
|
178
|
+
raise ImportError('to_frame needs pandas: pip install "algierex[pandas]"') from err
|
|
179
|
+
frame = pd.DataFrame(rows)
|
|
180
|
+
if index and index in frame.columns:
|
|
181
|
+
frame[index] = pd.to_datetime(frame[index])
|
|
182
|
+
frame = frame.set_index(index)
|
|
183
|
+
return frame
|
algierex/_types.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Response types. Generated by sdk/generate.mjs from the OpenAPI spec. Do not edit; run `npm run sdk`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
6
|
+
|
|
7
|
+
class Ticker(TypedDict):
|
|
8
|
+
ticker: str # Symbol, for example SAI.
|
|
9
|
+
nameEn: str # Name in English.
|
|
10
|
+
nameFr: Optional[str] # Name in French.
|
|
11
|
+
nameAr: Optional[str] # Name in Arabic.
|
|
12
|
+
descEn: Optional[str] # Short description in English.
|
|
13
|
+
descFr: Optional[str] # Short description in French.
|
|
14
|
+
descAr: Optional[str] # Short description in Arabic.
|
|
15
|
+
isin: Optional[str] # ISIN code.
|
|
16
|
+
sector: Optional[str] # Sector, for example pharma or banking.
|
|
17
|
+
listingDate: Optional[str] # First listing day.
|
|
18
|
+
sharesOutstanding: Optional[float] # Shares issued.
|
|
19
|
+
parValueDZD: Optional[float] # Par value in DZD.
|
|
20
|
+
freeFloatPct: Optional[float] # Percent of shares free to trade.
|
|
21
|
+
stateOwnedPct: Optional[float] # Percent held by the state.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PriceDay(TypedDict):
|
|
25
|
+
date: str # Session date.
|
|
26
|
+
sessionNumber: int # The exchange's session number.
|
|
27
|
+
market: Literal["main", "bond", "sme", "oat", "premium", "pi"] # Market the security traded in that day.
|
|
28
|
+
close: float # Closing price in DZD. For OATs, percent of face value.
|
|
29
|
+
open: Optional[float] # Opening price, when the bulletin gives one.
|
|
30
|
+
previousClose: Optional[float] # Previous close, when the bulletin gives one.
|
|
31
|
+
volume: float # Units traded.
|
|
32
|
+
value: float # Value traded in DZD.
|
|
33
|
+
transactions: int # Number of trades.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Session(TypedDict):
|
|
37
|
+
date: str # Session date.
|
|
38
|
+
sessionNumber: int # The exchange's session number.
|
|
39
|
+
market: Literal["main", "bond", "sme", "oat"] # Market.
|
|
40
|
+
volume: float # Units traded in the market that session.
|
|
41
|
+
value: float # Value traded in DZD.
|
|
42
|
+
transactions: int # Number of trades.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IndexDay(TypedDict):
|
|
46
|
+
date: str # Session date.
|
|
47
|
+
close: float # Index close.
|
|
48
|
+
change: float # Change from the previous close, in points.
|
|
49
|
+
changePct: float # Change from the previous close, in percent.
|
|
50
|
+
previousClose: float # Previous close.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class YieldCurvePointsItem(TypedDict):
|
|
54
|
+
ticker: str # OAT symbol.
|
|
55
|
+
maturityDate: str # Maturity date.
|
|
56
|
+
yearsToMaturity: float # Years from asOf to maturity.
|
|
57
|
+
yieldRate: float # Yield in percent at the bond's most recent trade.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
YieldCurveFitCurveItem = TypedDict("YieldCurveFitCurveItem", {
|
|
61
|
+
"years": float, # Maturity in years.
|
|
62
|
+
"yield": float, # Fitted yield in percent.
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# null when fewer than five bonds traded.
|
|
67
|
+
YieldCurveFit = TypedDict("YieldCurveFit", {
|
|
68
|
+
"beta0": float, # Long-term level.
|
|
69
|
+
"beta1": float, # Short-term component.
|
|
70
|
+
"beta2": float, # Curvature (medium-term hump).
|
|
71
|
+
"lambda": float, # Decay, in years: where the curvature peaks.
|
|
72
|
+
"rmse": float, # Root mean squared error of the fit, in percentage points.
|
|
73
|
+
"rSquared": float, # Share of yield variance across the bonds that the curve explains.
|
|
74
|
+
"shortRate": float, # Model yield at maturity zero: beta0 + beta1.
|
|
75
|
+
"longRate": float, # Model yield at very long maturity: beta0.
|
|
76
|
+
"curve": List[YieldCurveFitCurveItem], # The fitted curve sampled across the observed maturities.
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class YieldCurve(TypedDict):
|
|
81
|
+
asOf: str # The OAT session the curve is dated to.
|
|
82
|
+
staleCount: int # OATs listed in that session's bulletin with no trade in the last 60 OAT sessions. They are left out of the fit.
|
|
83
|
+
points: List[YieldCurvePointsItem] # The bonds behind the fit, shortest maturity first.
|
|
84
|
+
fit: Optional[YieldCurveFit] # null when fewer than five bonds traded.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class FxLatest(TypedDict):
|
|
88
|
+
assetCode: str # Currency code, for example USD.
|
|
89
|
+
assetType: Literal["CURRENCY", "CRYPTO", "METAL"] # Kind of asset.
|
|
90
|
+
source: Literal["official", "exchangedz"] # official: Bank of Algeria. exchangedz: parallel market.
|
|
91
|
+
date: str # Date of the rate.
|
|
92
|
+
buyRate: float # Buy rate in DZD per unit.
|
|
93
|
+
sellRate: float # Sell rate in DZD per unit.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CalendarEventNews(TypedDict):
|
|
97
|
+
"""The news item the date comes from; null for curated events."""
|
|
98
|
+
|
|
99
|
+
source: str # News origin.
|
|
100
|
+
sourceId: str # News id at the origin.
|
|
101
|
+
title: str # Title of the news item, in the asked language.
|
|
102
|
+
url: str # The article on Algierex.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CalendarEvent(TypedDict):
|
|
106
|
+
id: str # Stable id of the event.
|
|
107
|
+
date: str # Day of the event.
|
|
108
|
+
kind: str # ago: ordinary general meeting. agex: extraordinary general meeting. dividend_payment and coupon_payment: payment day. subscription_open and subscription_close: an offer's subscription period. listing_start: first trading day. session_cancelled: no session that day. result: results publication. deadline: a filing or application deadline. other: anything else.
|
|
109
|
+
label: str # What happens, in the asked language.
|
|
110
|
+
ticker: Optional[str] # The security, when the event is about one.
|
|
111
|
+
market: Optional[str] # The security's market, for curated events.
|
|
112
|
+
news: Optional[CalendarEventNews] # The news item the date comes from; null for curated events.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class NewsItem(TypedDict):
|
|
116
|
+
source: str # cosob-visa: COSOB approval of an offer document. cosob-decision: COSOB decision. cosob-communique: COSOB press release. sgbv-avis: the exchange's official notice. sgbv-press: the exchange's press and information notes. bank-of-algeria: Bank of Algeria publication. issuer-ir: a company's own investor news. editorial: written by Algierex.
|
|
117
|
+
sourceId: str # Id at the origin.
|
|
118
|
+
title: str # Title in the asked language.
|
|
119
|
+
summary: Optional[str] # Short summary in the asked language.
|
|
120
|
+
ticker: Optional[str] # The security the item is about.
|
|
121
|
+
market: Optional[str] # That security's market.
|
|
122
|
+
tags: List[str] # Topics, for example listing or dividend.
|
|
123
|
+
originalLanguage: Literal["fr", "en", "ar"] # Language the origin published in.
|
|
124
|
+
publishedAt: str # Publication time (UTC).
|
|
125
|
+
dateIsApprox: bool # True when the origin gave no exact date.
|
|
126
|
+
sourceUrl: str # Page at the origin.
|
|
127
|
+
pdfUrl: Optional[str] # The original document, when there is one.
|
|
128
|
+
url: str # The article on Algierex, in the asked language.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class FxRow(TypedDict):
|
|
132
|
+
date: str # Date of the rate.
|
|
133
|
+
assetCode: str # Currency code.
|
|
134
|
+
source: Literal["official", "exchangedz"] # official: Bank of Algeria. exchangedz: parallel market.
|
|
135
|
+
buyRate: float # Buy rate in DZD per unit.
|
|
136
|
+
sellRate: float # Sell rate in DZD per unit.
|
algierex/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: algierex
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the Algierex market data API (the Algerian exchange)
|
|
5
|
+
Author-email: Algierex <contact@algierex.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://algierex.com/developers
|
|
8
|
+
Project-URL: Documentation, https://algierex.com/developers/docs
|
|
9
|
+
Keywords: algeria,algiers,stock exchange,market data,finance,api
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# algierex (Python)
|
|
22
|
+
|
|
23
|
+
Python SDK for the [Algierex market data API](https://algierex.com/developers/docs): the Algerian exchange's daily prices back to 1999, sessions, the Dzair index, the OAT yield curve, exchange rates, company events and news.
|
|
24
|
+
|
|
25
|
+
No dependencies. Python 3.9 or newer.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install algierex # or "algierex[pandas]" for to_frame()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get a key
|
|
34
|
+
|
|
35
|
+
Create a key and choose a plan at [algierex.com/developers](https://algierex.com/developers). Then:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
export ALGIEREX_API_KEY=alx_...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Example
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from algierex import Algierex, PaymentRequiredError, to_frame
|
|
45
|
+
|
|
46
|
+
client = Algierex() # reads ALGIEREX_API_KEY
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
client.list_tickers()
|
|
50
|
+
except PaymentRequiredError as err:
|
|
51
|
+
raise SystemExit(f"{err}") # no active plan: choose one at algierex.com/developers
|
|
52
|
+
|
|
53
|
+
# Every daily close of Saidal since 2025, as a pandas DataFrame.
|
|
54
|
+
prices = to_frame(client.get_prices("SAI", from_="2025-01-01"))
|
|
55
|
+
print(prices["close"].tail())
|
|
56
|
+
|
|
57
|
+
# The OAT yield curve on a past date.
|
|
58
|
+
curve = client.get_yield_curve(date="2026-09-30")
|
|
59
|
+
print(curve["asOf"], curve["fit"]["rSquared"] if curve["fit"] else "not enough trades")
|
|
60
|
+
|
|
61
|
+
# Upcoming company events, in French.
|
|
62
|
+
for event in client.get_calendar(from_="2026-11-01", locale="fr"):
|
|
63
|
+
print(event["date"], event["ticker"], event["label"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Methods
|
|
67
|
+
|
|
68
|
+
| Method | Route |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `list_tickers()` | `GET /tickers` |
|
|
71
|
+
| `get_prices(ticker, from_=, to=)` | `GET /prices/{ticker}` |
|
|
72
|
+
| `list_sessions(market=, from_=, to=)` | `GET /sessions` |
|
|
73
|
+
| `get_dzair_index(from_=, to=)` | `GET /index/dzair` |
|
|
74
|
+
| `get_yield_curve(date=)` | `GET /yield-curve` |
|
|
75
|
+
| `list_fx()` | `GET /fx` |
|
|
76
|
+
| `get_fx_history(code, source=, from_=, to=)` | `GET /fx/{code}` |
|
|
77
|
+
| `get_calendar(from_=, to=, ticker=, locale=)` | `GET /calendar` |
|
|
78
|
+
| `list_news(ticker=, source=, from_=, to=, locale=, include_routine=)` | `GET /news` |
|
|
79
|
+
|
|
80
|
+
`from` is a Python keyword, so the parameter is `from_`. Every response is plain JSON (dicts and lists), typed with `TypedDict`s.
|
|
81
|
+
|
|
82
|
+
## Paging
|
|
83
|
+
|
|
84
|
+
Paging is automatic: list methods return every row. For long histories, the `iter_*` twin yields rows as pages arrive:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
for day in client.iter_prices("SAI"):
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`page_size` (1 to 1000, default 1000) sets rows per request; the default keeps calls against your plan's limits low.
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
Every error raises a subclass of `AlgierexError`, with `.status` and `.code`:
|
|
96
|
+
|
|
97
|
+
| Exception | When |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `BadRequestError` | 400: a parameter is not valid |
|
|
100
|
+
| `AuthenticationError` | 401: the key is missing, wrong, or revoked |
|
|
101
|
+
| `PaymentRequiredError` | 402: no active plan, or the last payment failed |
|
|
102
|
+
| `NotFoundError` | 404: no such security, currency, or date |
|
|
103
|
+
| `RateLimitError` | 429: the plan's limit is used up; `.retry_after` in seconds |
|
|
104
|
+
|
|
105
|
+
On a 429 the client waits `Retry-After` and retries, up to `max_retries` (default 3) and only for waits up to `max_retry_wait` seconds (default 60). `Algierex(max_retries=0)` raises at once. Server errors (5xx), timeouts (`timeout=30` seconds) and network failures raise `AlgierexError` and are not retried.
|
|
106
|
+
|
|
107
|
+
## Regenerating
|
|
108
|
+
|
|
109
|
+
This SDK is generated from the API's OpenAPI spec. From the repository root, after `npm install`: `npm run sdk`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
algierex/__init__.py,sha256=MtUNGfpWPkrSg7d4Lk4MGt0einnUz-gbhDlQNVV912w,782
|
|
2
|
+
algierex/_client.py,sha256=PBfS7mIFlxB-oGa6FwKlsaJ2l9Q60J8rZlQrUpG0PnI,9836
|
|
3
|
+
algierex/_core.py,sha256=Ghe8jHM_0uOHZ05aT5GM2kiKquZRMARPaGzhdqDhUI0,6777
|
|
4
|
+
algierex/_types.py,sha256=jKP9FJEWo_o7cEwpFUH3feziYoSLMncSJjg5ivJP9qY,6527
|
|
5
|
+
algierex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
algierex-1.0.0.dist-info/licenses/LICENSE,sha256=jCLkdpnzpfFZw7MrI8Q7zgJBNJ7m0gQbvjRZzO0dxt4,1065
|
|
7
|
+
algierex-1.0.0.dist-info/METADATA,sha256=SgCjtsDwmuW9nMcblPN6McWZI-H0MsuyE-pf7HwxBcc,3983
|
|
8
|
+
algierex-1.0.0.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
9
|
+
algierex-1.0.0.dist-info/top_level.txt,sha256=ghws0k9rtRiVzmP7fSKLwhiz7omu1uGt_lX8NlDSOXM,9
|
|
10
|
+
algierex-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Algierex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
algierex
|