PYield 0.32.4__tar.gz → 0.32.6__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.
Files changed (50) hide show
  1. {pyield-0.32.4 → pyield-0.32.6}/PKG-INFO +1 -1
  2. pyield-0.32.6/pyield/__about__.py +1 -0
  3. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/__init__.py +16 -1
  4. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/historical.py +0 -1
  5. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/intraday.py +0 -1
  6. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/xml.py +59 -5
  7. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/trade.py +3 -0
  8. {pyield-0.32.4 → pyield-0.32.6}/pyield/fwd.py +40 -25
  9. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnf.py +2 -2
  10. pyield-0.32.4/pyield/__about__.py +0 -1
  11. {pyield-0.32.4 → pyield-0.32.6}/.gitignore +0 -0
  12. {pyield-0.32.4 → pyield-0.32.6}/LICENSE +0 -0
  13. {pyield-0.32.4 → pyield-0.32.6}/README.md +0 -0
  14. {pyield-0.32.4 → pyield-0.32.6}/pyield/__init__.py +0 -0
  15. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/__init__.py +0 -0
  16. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ettj.py +0 -0
  17. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ima.py +0 -0
  18. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/imaq.py +0 -0
  19. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ipca.py +0 -0
  20. {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/tpf.py +0 -0
  21. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/__init__.py +0 -0
  22. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/di.py +0 -0
  23. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/di_over.py +0 -0
  24. {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/common.py +0 -0
  25. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/__init__.py +0 -0
  26. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/auction.py +0 -0
  27. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/bcdata.py +0 -0
  28. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/ptax.py +0 -0
  29. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/repo.py +0 -0
  30. {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/vna.py +0 -0
  31. {pyield-0.32.4 → pyield-0.32.6}/pyield/bday.py +0 -0
  32. {pyield-0.32.4 → pyield-0.32.6}/pyield/conftest.py +0 -0
  33. {pyield-0.32.4 → pyield-0.32.6}/pyield/data_cache.py +0 -0
  34. {pyield-0.32.4 → pyield-0.32.6}/pyield/date_converter.py +0 -0
  35. {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/__init__.py +0 -0
  36. {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/br_holidays_new.txt +0 -0
  37. {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/br_holidays_old.txt +0 -0
  38. {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/brholidays.py +0 -0
  39. {pyield-0.32.4 → pyield-0.32.6}/pyield/ibge/__init__.py +0 -0
  40. {pyield-0.32.4 → pyield-0.32.6}/pyield/ibge/ipca.py +0 -0
  41. {pyield-0.32.4 → pyield-0.32.6}/pyield/interpolator.py +0 -0
  42. {pyield-0.32.4 → pyield-0.32.6}/pyield/py.typed +0 -0
  43. {pyield-0.32.4 → pyield-0.32.6}/pyield/retry.py +0 -0
  44. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/__init__.py +0 -0
  45. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/lft.py +0 -0
  46. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ltn.py +0 -0
  47. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnb.py +0 -0
  48. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnc.py +0 -0
  49. {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/tools.py +0 -0
  50. {pyield-0.32.4 → pyield-0.32.6}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.32.4
3
+ Version: 0.32.6
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Source, https://github.com/crdcj/PYield
6
6
  Author-email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.32.6"
@@ -1,4 +1,5 @@
1
1
  import datetime as dt
2
+ import logging
2
3
  from typing import Literal
3
4
  from zoneinfo import ZoneInfo
4
5
 
@@ -7,10 +8,12 @@ import pandas as pd
7
8
  from pyield import date_converter as dc
8
9
  from pyield.b3.futures.historical import fetch_historical_df
9
10
  from pyield.b3.futures.intraday import fetch_intraday_df
11
+ from pyield.b3.futures.xml import fetch_xml_report
10
12
  from pyield.date_converter import DateScalar
11
13
 
12
14
  TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
13
15
  ContractOptions = Literal["DI1", "DDI", "FRC", "DAP", "DOL", "WDO", "IND", "WIN"]
16
+ logger = logging.getLogger(__name__)
14
17
 
15
18
 
16
19
  def futures(
@@ -60,13 +63,25 @@ def futures(
60
63
  selected_contract = str(contract_code).upper()
61
64
  converted_date = dc.convert_input_dates(date)
62
65
 
63
- # First, try to fetch historical data for the specified date
66
+ # First, try to fetch historical data from BMF site
64
67
  df = fetch_historical_df(selected_contract, converted_date)
68
+ if df.empty:
69
+ # If historical data is not available, try to fetch the full XML report
70
+ # from the B3 site
71
+ try:
72
+ df = fetch_xml_report(converted_date, selected_contract, "PR")
73
+ logger.info(f"Using B3 XML report data for {selected_contract}")
74
+ except ValueError as e:
75
+ logger.error(f"Failed to fetch data for {selected_contract}: {e}")
76
+ raise
77
+ else:
78
+ logger.info(f"Using BMF historical data for {selected_contract}")
65
79
 
66
80
  bz_today = dt.datetime.now(TIMEZONE_BZ).date()
67
81
  # If there is no historical data available, try to fetch intraday data
68
82
  if converted_date.date() == bz_today and df.empty:
69
83
  df = fetch_intraday_df(selected_contract)
84
+ logger.info(f"Using B3 intraday data for {selected_contract}")
70
85
 
71
86
  return df
72
87
 
@@ -245,7 +245,6 @@ def process_df(
245
245
  duration = df["BDaysToExp"] / 252
246
246
  modified_duration = duration / (1 + df["SettlementRate"])
247
247
  df["DV01"] = 0.0001 * modified_duration * df["SettlementPrice"]
248
- df["DV01"] = df["DV01"].astype("Float64")
249
248
 
250
249
  return df
251
250
 
@@ -129,7 +129,6 @@ def _process_df(raw_df: pd.DataFrame) -> pd.DataFrame:
129
129
  duration = df["BDaysToExp"] / 252
130
130
  modified_duration = duration / (1 + df["LastRate"])
131
131
  df["DV01"] = 0.0001 * modified_duration * df["LastPrice"]
132
- df["DV01"] = df["DV01"].astype("Float64")
133
132
 
134
133
  return df
135
134
 
@@ -1,4 +1,5 @@
1
1
  import io
2
+ import logging
2
3
  import zipfile
3
4
  from pathlib import Path
4
5
  from typing import Literal
@@ -10,6 +11,8 @@ from lxml import etree
10
11
  from pyield import bday
11
12
  from pyield.b3.futures import common
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
 
14
17
  def _get_file_from_path(file_path: Path) -> io.BytesIO:
15
18
  # Check if a file path was not provided
@@ -47,9 +50,13 @@ def _get_file_from_url(date: pd.Timestamp, source_type: str) -> io.BytesIO:
47
50
  response = requests.get(url, timeout=10)
48
51
 
49
52
  # File will be considered invalid if it is too small
50
- if response.status_code != 200 or len(response.content) < 1024:
53
+ if response.status_code != 200 or len(response.content) < 1024: # noqa
51
54
  date_str = date.strftime("%Y-%m-%d")
52
- raise ValueError(f"There is no data available for {date_str}.")
55
+ logger.warning(
56
+ f"File not found or too small ({len(response.content)} bytes) "
57
+ f"for {source_type} on {date_str}. Returning empty file."
58
+ )
59
+ return io.BytesIO()
53
60
 
54
61
  return io.BytesIO(response.content)
55
62
 
@@ -102,7 +109,7 @@ def _extract_data_from_xml(xml_file: io.BytesIO, asset_code: str) -> list[dict]:
102
109
  continue
103
110
 
104
111
  # A future contract ticker must have 6 characters
105
- if ticker.text is None or len(ticker.text) != 6:
112
+ if ticker.text is None or len(ticker.text) != 6: # noqa
106
113
  continue
107
114
 
108
115
  # Extract the price report element
@@ -246,6 +253,10 @@ def _select_and_reorder_columns(df: pd.DataFrame) -> pd.DataFrame:
246
253
 
247
254
 
248
255
  def process_zip_file(zip_file: io.BytesIO, asset_code: str) -> pd.DataFrame:
256
+ if zip_file is None or zip_file.getbuffer().nbytes == 0:
257
+ logger.warning("Empty XML zip file. Returning empty DataFrame.")
258
+ return pd.DataFrame()
259
+
249
260
  xml_file = _extract_xml_from_zip(zip_file)
250
261
 
251
262
  di_data = _extract_data_from_xml(xml_file, asset_code)
@@ -258,18 +269,61 @@ def process_zip_file(zip_file: io.BytesIO, asset_code: str) -> pd.DataFrame:
258
269
 
259
270
  df = _select_and_reorder_columns(df)
260
271
 
272
+ if asset_code == "DI1":
273
+ duration = df["BDaysToExp"] / 252
274
+ modified_duration = duration / (1 + df["SettlementRate"])
275
+ df["DV01"] = 0.0001 * modified_duration * df["SettlementPrice"]
276
+
261
277
  return df
262
278
 
263
279
 
264
- def fetch_df(
280
+ def fetch_xml_report(
265
281
  date: pd.Timestamp, asset_code: str, source_type: Literal["PR", "SPR"]
266
282
  ) -> pd.DataFrame:
283
+ """Fetches and processes an XML report from B3's website.
284
+
285
+ Downloads a zipped XML report for a specific date and asset code
286
+ from B3's website, extracts the relevant data, and returns it as a
287
+ Pandas DataFrame.
288
+
289
+ Args:
290
+ date: The date of the report to fetch.
291
+ asset_code: The asset code to filter the report for (e.g., 'DI1').
292
+ source_type: The type of report to fetch, either 'PR' (Full Price
293
+ Report) or 'SPR' (Simplified Price Report).
294
+
295
+ Returns:
296
+ A Pandas DataFrame containing the processed data.
297
+ For 'DI1' asset codes, an additional 'DV01' column is calculated.
298
+
299
+ Raises:
300
+ ValueError: If the `source_type` is invalid or if no data is
301
+ available for the given date.
302
+ """
267
303
  zip_file = _get_file_from_url(date, source_type)
268
304
  df = process_zip_file(zip_file, asset_code)
305
+
269
306
  return df
270
307
 
271
308
 
272
- def read_df(file_path: Path, asset_code: str) -> pd.DataFrame:
309
+ def read_xml_report(file_path: Path, asset_code: str) -> pd.DataFrame:
310
+ """Reads and processes an XML report from a local file.
311
+
312
+ Reads a zipped XML report from the specified file path, extracts the
313
+ relevant data, and returns it as a Pandas DataFrame.
314
+
315
+ Args:
316
+ file_path: The path to the zipped XML report file.
317
+ asset_code: The asset code to filter the report for (e.g., 'DI1').
318
+
319
+ Returns:
320
+ A Pandas DataFrame containing the processed data.
321
+ For 'DI1' asset codes, an additional 'DV01' column is calculated.
322
+
323
+ Raises:
324
+ ValueError: If the provided `file_path` is not a Path object.
325
+ FileNotFoundError: If no file is found at the specified `file_path`.
326
+ """
273
327
  zip_file = _get_file_from_path(file_path)
274
328
  df = process_zip_file(zip_file, asset_code)
275
329
  return df
@@ -101,4 +101,7 @@ def tpf_trades(target_date: DateScalar) -> pd.DataFrame:
101
101
  url = _build_download_url(target_date)
102
102
  df = _fetch_data_from_url(url)
103
103
  df = df.rename(columns=COLUMN_MAPPING)
104
+
105
+ # TradeValue are empty in the original BCB file, so we calculate it
106
+ df["TradeValue"] = (df["TradeQuantity"] * df["AvgPrice"]).round(2)
104
107
  return df
@@ -6,34 +6,43 @@ def forwards(
6
6
  rates: pd.Series,
7
7
  groupby_dates: pd.Series | None = None,
8
8
  ) -> pd.Series:
9
- """
9
+ r"""
10
10
  Calcula taxas a termo (forward rates) a partir de taxas zero (spot rates).
11
11
 
12
- Utiliza a fórmula:
13
- f₁→₂ = ((1 + r₂)^(du₂/252) / (1 + r₁)^(du₁/252))^(252/(du₂ - du₁)) - 1
12
+ A taxa a termo no vértice 'n' é definida como:
13
+ fwdₙ = fwdₙ₋₁→ₙ (a taxa a termo de n-1 para n)
14
+
15
+ A fórmula utilizada é:
16
+ fwdₙ = ((1 + rₙ)^(duₙ/252) / (1 + rₙ₋₁)^(duₙ₋₁/252))^(252/(duₙ - duₙ₋₁)) - 1
14
17
 
15
18
  Como du/252 = t (tempo em anos úteis), a fórmula pode ser simplificada para:
16
- f₁→₂ = ((1 + r₂)^t₂ / (1 + r₁)^t₁)^(1/(t₂ - t₁)) - 1
19
+
20
+ fwdₙ = ((1 + rₙ)^tₙ / (1 + rₙ₋₁)^tₙ₋₁)^(1/(tₙ - tₙ₋₁)) - 1
21
+
22
+ Em LaTeX, a fórmula é representada como:
23
+ $$
24
+ fwd_{n} = \left( \frac{(1 + r_n)^{t_n}}{(1 + r_{n-1})^{t_{n-1}}} \right)^{\frac{1}{t_n - t_{n-1}}} - 1
25
+ $$
17
26
 
18
27
  Onde:
19
- - r₁ é a taxa zero para o vértice anterior.
20
- - r₂ é a taxa zero para o vértice atual.
21
- - t₁ é o prazo em anos para o vértice anterior (calculado como du₁/252).
22
- - t₂ é o prazo em anos para o vértice atual (calculado como du₂/252).
28
+ - rₙ₋₁ é a taxa zero para o vértice anterior.
29
+ - rₙ é a taxa zero para o vértice atual.
30
+ - tₙ₋₁ é o prazo em anos para o vértice anterior (calculado como duₙ₋₁/252).
31
+ - tₙ é o prazo em anos para o vértice atual (calculado como duₙ/252).
23
32
  - A constante 252 representa o número de dias úteis no ano.
24
33
 
25
- A primeira taxa a termo de cada sequência (ou grupo) é definida como a
26
- taxa zero desse primeiro vértice (f₁→₂ = r₂), dado que não existe um vértice
27
- anterior (r₁) para se calcular a taxa a termo do primeiro ponto.
28
-
29
- A função lida internamente com o alinhamento dos índices das Séries de
30
- entrada através de `reset_index`. Os cálculos são realizados após ordenar
31
- os dados pelos prazos (`bdays`) dentro de cada grupo definido por
32
- `groupby_dates` (ou como um único grupo se `groupby_dates` não for
33
- fornecido), garantindo a ordem cronológica correta para a fórmula.
34
+ A primeira taxa a termo de cada grupo é definida como a
35
+ taxa zero desse primeiro vértice (fwd₁ = r₁), dado que não existe um vértice
36
+ anterior a r₁ para se calcular a taxa a termo no primeiro ponto.
34
37
 
35
- Valores NaN nas taxas ou prazos de entrada resultarão em NaN na saída,
36
- exceto para o primeiro ponto de cada grupo, que é tratado separadamente.
38
+ Valores nulos nas taxas ou prazos de entrada resultarão em valores nulos
39
+ nas taxas a termo calculadas. A função também lida com agrupamentos
40
+ opcionais, permitindo calcular taxas a termo para diferentes grupos de
41
+ datas. O agrupamento é feito com base na coluna `groupby_dates`, que
42
+ deve ser fornecida como uma série de pandas. Se `groupby_dates` for None,
43
+ todos os dados serão tratados como um único grupo.
44
+ A função calcula as taxas a termo para todos os pontos, exceto o primeiro
45
+ de cada grupo, que é tratado separadamente.
37
46
 
38
47
  Args:
39
48
  bdays (pd.Series): Número de dias úteis para cada taxa zero.
@@ -47,7 +56,7 @@ def forwards(
47
56
  pd.Series: Série contendo as taxas a termo calculadas (tipo Float64).
48
57
  A primeira taxa de cada grupo corresponde à taxa zero inicial.
49
58
 
50
- Example:
59
+ Examples:
51
60
  >>> bdays = pd.Series([10, 20, 30])
52
61
  >>> rates = pd.Series([0.05, 0.06, 0.07])
53
62
  >>> yd.forwards(bdays, rates)
@@ -55,7 +64,7 @@ def forwards(
55
64
  1 0.070095
56
65
  2 0.090284
57
66
  dtype: Float64
58
- """
67
+ """ # noqa: E501
59
68
  # Reset Series indexes to avoid misalignment issues during calculations
60
69
  bdays = bdays.reset_index(drop=True).astype("Int64")
61
70
  rates = rates.reset_index(drop=True).astype("Float64")
@@ -104,7 +113,7 @@ def forward(
104
113
  rate1: float,
105
114
  rate2: float,
106
115
  ) -> float:
107
- """
116
+ r"""
108
117
  Calcula a taxa a termo (forward rate) entre dois prazos (dias úteis).
109
118
 
110
119
  Utiliza a fórmula:
@@ -118,6 +127,7 @@ def forward(
118
127
  - A constante 252 representa o número de dias úteis no ano.
119
128
 
120
129
  Como du/252 = t (tempo em anos úteis), a fórmula pode ser simplificada para:
130
+
121
131
  f₁→₂ = ((1 + r₂)^t₂ / (1 + r₁)^t₁)^(1/(t₂ - t₁)) - 1
122
132
 
123
133
  Args:
@@ -131,8 +141,8 @@ def forward(
131
141
  `float('nan')` se `bday2 <= bday1` ou se qualquer um dos
132
142
  argumentos de entrada for NaN.
133
143
 
134
- Example:
135
- >>> # Exemplo correto: bday2 > bday1
144
+ Examples:
145
+ >>> # Exemplo válido: bday2 > bday1
136
146
  >>> yd.forward(10, 20, 0.05, 0.06)
137
147
  0.0700952380952371
138
148
  >>> # Exemplo inválido: bday2 <= bday1
@@ -147,7 +157,12 @@ def forward(
147
157
  Note:
148
158
  É fundamental que `bday2` seja estritamente maior que `bday1` para que
149
159
  o cálculo da taxa a termo seja matematicamente válido.
150
- """
160
+
161
+ A fórmula utilizada é derivada da relação entre taxas zero (spot rates) é:
162
+ $$
163
+ f_{1 \rightarrow 2} = \left( \frac{(1 + r_2)^{t_2}}{(1 + r_1)^{t_1}} \right)^{\frac{1}{t_2 - t_1}} - 1
164
+ $$
165
+ """ # noqa: E501
151
166
  if pd.isna(rate1) or pd.isna(rate2) or pd.isna(bday1) or pd.isna(bday2):
152
167
  # If any of the inputs are NaN, return NaN
153
168
  return float("nan")
@@ -20,7 +20,7 @@ FINAL_PMT = FACE_VALUE + COUPON_PMT
20
20
  COUPON_DAY = 1
21
21
  COUPON_MONTHS = {1, 7}
22
22
  COUPON_PMT = 48.80885
23
- FINAL_PMT = 1048.80885
23
+ FINAL_PMT = 1048.80885 # 1000 + 48.80885
24
24
 
25
25
 
26
26
  def data(date: DateScalar) -> pd.DataFrame:
@@ -364,8 +364,8 @@ def di_spreads(date: DateScalar) -> pd.DataFrame:
364
364
  return df[["MaturityDate", "DISpread"]]
365
365
 
366
366
 
367
- # Bisection method to find the root
368
367
  def _bisection_method(func, a, b, tol=1e-8, maxiter=100):
368
+ """Bisection method for root finding."""
369
369
  fa, fb = func(a), func(b)
370
370
  if fa * fb > 0:
371
371
  raise ValueError("Function does not change sign in the interval.")
@@ -1 +0,0 @@
1
- __version__ = "0.32.4"
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
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
File without changes