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.
- {pyield-0.32.4 → pyield-0.32.6}/PKG-INFO +1 -1
- pyield-0.32.6/pyield/__about__.py +1 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/__init__.py +16 -1
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/historical.py +0 -1
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/intraday.py +0 -1
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/xml.py +59 -5
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/trade.py +3 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/fwd.py +40 -25
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnf.py +2 -2
- pyield-0.32.4/pyield/__about__.py +0 -1
- {pyield-0.32.4 → pyield-0.32.6}/.gitignore +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/LICENSE +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/README.md +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ettj.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ima.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/imaq.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/ipca.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/anbima/tpf.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/di.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/di_over.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/b3/futures/common.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/auction.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/bcdata.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/ptax.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/repo.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bc/vna.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/bday.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/conftest.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/data_cache.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/date_converter.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/ibge/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/ibge/ipca.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/interpolator.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/py.typed +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/retry.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/__init__.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/lft.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ltn.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnb.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/ntnc.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyield/tpf/tools.py +0 -0
- {pyield-0.32.4 → pyield-0.32.6}/pyproject.toml +0 -0
|
@@ -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
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
20
|
-
- r
|
|
21
|
-
- t
|
|
22
|
-
- t
|
|
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
|
|
26
|
-
taxa zero desse primeiro vértice (
|
|
27
|
-
anterior
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
>>> # Exemplo
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|