luxorasap 0.0.1__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.
- luxorasap/__init__.py +20 -0
- luxorasap/datareader/__init__.py +7 -0
- luxorasap/datareader/core.py +2996 -0
- luxorasap-0.0.1.dist-info/METADATA +70 -0
- luxorasap-0.0.1.dist-info/RECORD +8 -0
- luxorasap-0.0.1.dist-info/WHEEL +5 -0
- luxorasap-0.0.1.dist-info/entry_points.txt +2 -0
- luxorasap-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2996 @@
|
|
|
1
|
+
# Imports
|
|
2
|
+
__version__ = "0.0.1"
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import datetime as dt
|
|
6
|
+
from datetime import timezone
|
|
7
|
+
from loguru import logger
|
|
8
|
+
import os, sys
|
|
9
|
+
import time
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy.optimize import newton
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import io
|
|
14
|
+
try:
|
|
15
|
+
from azure.storage.blob import BlobServiceClient
|
|
16
|
+
import pyarrow as pa
|
|
17
|
+
import pyarrow.parquet as pq
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
load_dotenv()
|
|
20
|
+
except ImportError:
|
|
21
|
+
print("Favor instalar as dependencias abaixo:")
|
|
22
|
+
print('pip install azure-storage-blob')
|
|
23
|
+
print('pip install pyarrow')
|
|
24
|
+
print('pip install python-dotenv')
|
|
25
|
+
sys.exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#ADLS_CONNECTION_STRING = os.getenv('AZURE_STORAGE_CONNECTION_STRING')
|
|
29
|
+
|
|
30
|
+
#@logger.catch
|
|
31
|
+
class LuxorQuery:
|
|
32
|
+
|
|
33
|
+
def __init__(self, update_mode="optimized", is_develop_mode=False, tables_path=None,
|
|
34
|
+
blob_directory='enriched/parquet', adls_connection_string:str=None, container_name="luxorasap"):
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
update_mode:
|
|
38
|
+
'standard' - Carrega todas as tabelas disponiveis
|
|
39
|
+
'optimized' - Carrega apenas as tabelas utilizadas sob demanda
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if adls_connection_string is None:
|
|
43
|
+
adls_connection_string = os.getenv('AZURE_STORAGE_CONNECTION_STRING')
|
|
44
|
+
|
|
45
|
+
self.blob_directory = blob_directory
|
|
46
|
+
self.container_name = container_name
|
|
47
|
+
|
|
48
|
+
self.CONNECTION_STRING = adls_connection_string
|
|
49
|
+
self.blob_service_client = BlobServiceClient.from_connection_string(self.CONNECTION_STRING)
|
|
50
|
+
self.container_client = self.blob_service_client.get_container_client(self.container_name)
|
|
51
|
+
|
|
52
|
+
self.modified_tables = []
|
|
53
|
+
self.is_develop_mode = is_develop_mode
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
self.tables_path = tables_path
|
|
57
|
+
#if tables_path is None:
|
|
58
|
+
# self.tables_path = self.__set_tables_path()
|
|
59
|
+
|
|
60
|
+
self.tables_in_use = {}
|
|
61
|
+
self.asset_last_prices = {}
|
|
62
|
+
self.price_cache = {} # otimizacao da consulta de preco
|
|
63
|
+
self.price_tables_loaded = {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
self.lipi_manga_incorp_date = dt.date(2022,12,9)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
self.update_modes_name = {"standard" : 0, "optimized" : 1}
|
|
70
|
+
self.update_mode = self.update_modes_name[update_mode]
|
|
71
|
+
self.update() # Nessa 1° exec. vai inicializar os dicionarios acima
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
#def __set_tables_path(self):
|
|
76
|
+
#
|
|
77
|
+
# cur_dir = Path().absolute()
|
|
78
|
+
# cur_dir_items = cur_dir.parts
|
|
79
|
+
# home = cur_dir.home()
|
|
80
|
+
# onedrive_name = cur_dir_items[3]
|
|
81
|
+
# if "OneDrive".lower() not in onedrive_name.lower():
|
|
82
|
+
# logger.critical("No formato atual, para utilizar esse modulo a main deve ser executada dentro do diretorio do OneDrive.")
|
|
83
|
+
# #formato considerado eh: "C:/Users/{user}/{onedrive_name}"
|
|
84
|
+
#
|
|
85
|
+
# tables_path = Path(home)/onedrive_name/"projetos"/"LuxorASAP"/"luxorDB"/"tables"
|
|
86
|
+
# if self.is_develop_mode:
|
|
87
|
+
# tables_path = Path(home)/onedrive_name/"projetos"/"LuxorASAP_Develop"/"luxorDB"/"tables"
|
|
88
|
+
#
|
|
89
|
+
# return tables_path
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def __get_blob_update_time(self, table_name):
|
|
93
|
+
|
|
94
|
+
blob_name = f"{self.blob_directory}/{table_name}.parquet"
|
|
95
|
+
blob_client = self.blob_service_client.get_blob_client(container=self.container_name, blob=blob_name)
|
|
96
|
+
|
|
97
|
+
# Obter as propriedades do blob
|
|
98
|
+
properties = blob_client.get_blob_properties()
|
|
99
|
+
return properties['last_modified'].replace(tzinfo=timezone.utc).timestamp()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def __is_table_modified(self, table_name):
|
|
103
|
+
""" Retorna 'True' ou 'False' informando se a tabela informada em 'table_name' foi criada ou modificada.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
table_name (str): nome tabela
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
bool: True se foi criada ou modificada
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if table_name not in self.tables_in_use:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
file_path = self.tables_in_use[table_name]["table_path"]
|
|
117
|
+
file_last_update = self.__get_blob_update_time(table_name)
|
|
118
|
+
return file_last_update > self.tables_in_use[table_name]["update_time"]
|
|
119
|
+
|
|
120
|
+
except:
|
|
121
|
+
logger.critical(f"Arquivo <{file_path}> não encontrado.")
|
|
122
|
+
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def __persist_column_formatting(self, t):
|
|
127
|
+
|
|
128
|
+
columns_to_persist = {"Name", "Class", "Vehicles", "Segment"}
|
|
129
|
+
|
|
130
|
+
if len(set(t.columns).intersection(columns_to_persist)) > 0:
|
|
131
|
+
# Vamos persistir a formatacao de algumas colunas
|
|
132
|
+
columns_order = list(t.columns)
|
|
133
|
+
columns_to_persist = list(set(t.columns).intersection(columns_to_persist))
|
|
134
|
+
persistent_data = t[columns_to_persist].copy()
|
|
135
|
+
|
|
136
|
+
columns_to_normalize = list(set(columns_order) - set(columns_to_persist))
|
|
137
|
+
t = self.text_to_lowercase(t[columns_to_normalize])
|
|
138
|
+
t.loc[:,columns_to_persist] = persistent_data
|
|
139
|
+
return t[columns_order]
|
|
140
|
+
|
|
141
|
+
# Nos outros casos, transformaremos tudo em lowercase
|
|
142
|
+
return self.text_to_lowercase(t)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def __get_tickers_bbg(self):
|
|
146
|
+
# Deprecated.
|
|
147
|
+
# Criado apenas para manter compatibilidade
|
|
148
|
+
logger.warning("Acesso direto a tabela 'bbg_ticker' sera descontinuado.\nAo inves disso, pegue os valores unicos de asset[Ticker_BBG]")
|
|
149
|
+
return pd.DataFrame(self.get_table("assets")["Ticker_BBG"].unique())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def table_exists(self, table_name, blob_directory=None):
|
|
153
|
+
# Checa no ADLS se existe alguma tabela com o nome informado
|
|
154
|
+
|
|
155
|
+
table_path = f"{blob_directory}/{table_name}.parquet"
|
|
156
|
+
if blob_directory is None:
|
|
157
|
+
blob_directory = self.blob_directory
|
|
158
|
+
table_path = f"{blob_directory}/{table_name}.parquet"
|
|
159
|
+
|
|
160
|
+
blob_client = self.container_client.get_blob_client(table_path)
|
|
161
|
+
|
|
162
|
+
return blob_client.exists()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_table(self, table_name, index=False, index_name="index", dtypes_override={}, force_reload=False):
|
|
166
|
+
"""
|
|
167
|
+
Retorna uma copia do DataFrame do 'table_name' correspondente. Se não estiver disponivel,
|
|
168
|
+
retorna None.
|
|
169
|
+
|
|
170
|
+
table_name:
|
|
171
|
+
'px_last' - Último preco dos ativos\n
|
|
172
|
+
'trades' - Historico de boletas dos trades da Luxor\n
|
|
173
|
+
'assets' - Tabela de ativos validos\n
|
|
174
|
+
'hist_px_last' - Preço histórico dos ativos desde 1990\n
|
|
175
|
+
'hist_vc_px_last' - Preço histórico dos VCs\n
|
|
176
|
+
'hist_non_bbg_px_last' - Preço histórico de ativos sem preco no bbg (fidc trybe, spx hawker)\n
|
|
177
|
+
'[NOME_FUNDO]_quotas - Cotas historicas do fundo (fund_a, fund_b, hmx, ...\n
|
|
178
|
+
'hist_us_cash' - Historico de caixas dos fundos (us apenas)\n
|
|
179
|
+
'bbg_tickers' - Ticker bbg de todos os tickers cadastrados\n
|
|
180
|
+
'bdr_sizes' - Tabela com o ultimo peso de cada bdr\n
|
|
181
|
+
'hist_swap' - Tabela historica dos swaps\n
|
|
182
|
+
'hist_positions' - Historico de posicoes dos fundos\n
|
|
183
|
+
'hist_positions_by_bank- Posicoes por banco\n
|
|
184
|
+
'cash_movements' - Historico de movimentacoes com data de liquidacao\n
|
|
185
|
+
'holidays' - Tabela de feriados das bolsas\n
|
|
186
|
+
'daily_pnl' - Tabela de PnL diario\n
|
|
187
|
+
'custom_funds_quotas' - Cotas dos fundos customizados(luxor equities, non-equities, etc)\n
|
|
188
|
+
|
|
189
|
+
dtypes_override: dict : set - Dicionario com os tipos de dados das colunas devem ser sobrescritos.
|
|
190
|
+
Deve possuir as chaves 'float', 'date', 'bool' e 'str_nan_format'(troca 'nan' por pd.NA)
|
|
191
|
+
Para cada chave, colocar um Set com os nomes das colunas que receberao o cast.
|
|
192
|
+
"""
|
|
193
|
+
table_name = table_name.lower().replace(" ", "_")
|
|
194
|
+
if table_name == 'bbg_tickers': return self.__get_tickers_bbg() # DEPRECATED TODO: remover apos testes
|
|
195
|
+
|
|
196
|
+
if (table_name in self.tables_in_use) and not force_reload:
|
|
197
|
+
return self.tables_in_use[table_name]["table_data"]
|
|
198
|
+
|
|
199
|
+
return self.__load_table(table_name, index=index, index_name=index_name, dtypes_override=dtypes_override)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def __load_table(self, table_name, index=False, index_name="index", dtypes_override={}):
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def __read_blob_parquet(table_name):
|
|
207
|
+
|
|
208
|
+
container_name = "luxorasap"
|
|
209
|
+
blob_name = f"{self.blob_directory}/{table_name}.parquet"
|
|
210
|
+
|
|
211
|
+
blob_client = self.blob_service_client.get_blob_client(container=container_name, blob=blob_name)
|
|
212
|
+
|
|
213
|
+
# Download do blob em memória
|
|
214
|
+
download_stream = None
|
|
215
|
+
try:
|
|
216
|
+
download_stream = blob_client.download_blob()
|
|
217
|
+
except Exception:
|
|
218
|
+
print(f"Tabela '{table_name}' não encontrada no blob.")
|
|
219
|
+
return None, False
|
|
220
|
+
parquet_data = download_stream.readall()
|
|
221
|
+
|
|
222
|
+
# Ler o parquet do stream em memória
|
|
223
|
+
parquet_buffer = io.BytesIO(parquet_data)
|
|
224
|
+
table = pq.read_table(parquet_buffer)
|
|
225
|
+
df = table.to_pandas()
|
|
226
|
+
|
|
227
|
+
return df, True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def __load_parquet(table_name):
|
|
231
|
+
table_path = f"{self.blob_directory}/{table_name}.parquet"#self.tables_path/"parquet"/f"{table_name}.parquet"
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
#update_time = os.path.getmtime(table_path)
|
|
235
|
+
update_time = self.__get_blob_update_time(table_name)
|
|
236
|
+
table_data = None
|
|
237
|
+
# Primeiro, vamos tentar ler do blob
|
|
238
|
+
table_data, blob_read_success = __read_blob_parquet(table_name)
|
|
239
|
+
|
|
240
|
+
if not blob_read_success:
|
|
241
|
+
logger.critical(f"Não foi possível carregar a tabela '{table_name}' do blob.")
|
|
242
|
+
#print("--> Onedrive fallback.")
|
|
243
|
+
#table_data = pd.read_parquet(table_path,engine="fastparquet")
|
|
244
|
+
|
|
245
|
+
table_columns = set(table_data.columns)
|
|
246
|
+
|
|
247
|
+
float_dtypes = {"Last_Price", "Price", "px_last", "Quota", "#", "Avg_price", "Variation", "Variation_tot",
|
|
248
|
+
"Value", "%_MoM", "PL", "AUM", "%_pl", "Market_Value", "P&L_MoM", "Debt", "Kd", "P&L_YTD", "Return_Multiplier",
|
|
249
|
+
"Weight", "%_YTD", "Net_Debt/Ebitda", "adr_adr_per_sh", "Volume", "Total","Liquidity_Rule",
|
|
250
|
+
"Daily_Return", "Amount", "Adjust", "Year", "Daily_Adm_Fee", "Timestamp",
|
|
251
|
+
"Pnl", "Attribution"}
|
|
252
|
+
if 'float' in dtypes_override:
|
|
253
|
+
float_dtypes = float_dtypes.union(dtypes_override['float'])
|
|
254
|
+
|
|
255
|
+
date_dtypes = {"Date", "last_update_dt", "Query_Time", "update_time", "Update_Time",
|
|
256
|
+
"Settlement Date", "Today"}
|
|
257
|
+
if 'date' in dtypes_override:
|
|
258
|
+
date_dtypes = date_dtypes.union(dtypes_override['date'])
|
|
259
|
+
|
|
260
|
+
bool_dtypes = {"Ignore Flag", "Disable BDP", "Hist_Price", "Historical_Data"}
|
|
261
|
+
if 'bool' in dtypes_override:
|
|
262
|
+
bool_dtypes = bool_dtypes.union(dtypes_override['bool'])
|
|
263
|
+
|
|
264
|
+
str_nan_format = {"Ticker_BBG"}
|
|
265
|
+
if 'str_nan_format' in dtypes_override:
|
|
266
|
+
str_nan_format = str_nan_format.union(dtypes_override['str_nan_format'])
|
|
267
|
+
|
|
268
|
+
if table_name != "last_all_flds":
|
|
269
|
+
for col in table_columns.intersection(float_dtypes):
|
|
270
|
+
table_data[col] = table_data[col].astype(float)
|
|
271
|
+
|
|
272
|
+
if table_name == "hist_risk_metrics":
|
|
273
|
+
try:
|
|
274
|
+
cols_to_format = list(set(table_data.columns) - {"Date", "Fund"})
|
|
275
|
+
table_data[cols_to_format] = table_data[cols_to_format].astype(float)
|
|
276
|
+
except ValueError:
|
|
277
|
+
logger.warning("Ao carregar tabela 'hist_px_last', nao foi possivel converter dados para float.")
|
|
278
|
+
|
|
279
|
+
for col in table_columns.intersection(date_dtypes):
|
|
280
|
+
try:
|
|
281
|
+
table_data[col] = pd.to_datetime(table_data[col], format="mixed")
|
|
282
|
+
|
|
283
|
+
except ValueError:
|
|
284
|
+
table_data[col] = table_data[col].apply(lambda x: pd.to_datetime(x))
|
|
285
|
+
|
|
286
|
+
for col in table_columns.intersection(bool_dtypes):
|
|
287
|
+
table_data[col] = (table_data[col].str.lower()
|
|
288
|
+
.replace("false", "").replace("falso", "")
|
|
289
|
+
.replace("0", "").replace("nan", "").astype(bool))
|
|
290
|
+
|
|
291
|
+
for col in table_columns.intersection(str_nan_format):
|
|
292
|
+
table_data[col] = table_data[col].replace("nan", pd.NA).replace("", pd.NA)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
return table_data.copy(), table_path, update_time
|
|
297
|
+
|
|
298
|
+
except Exception:
|
|
299
|
+
logger.critical(f"Nao foi possivel carregar a tabela <{table_name}>.")
|
|
300
|
+
return None, None, None
|
|
301
|
+
|
|
302
|
+
#def __load_csv(table_name):
|
|
303
|
+
# table_path = self.tables_path/"csv"/f"{table_name}.csv"
|
|
304
|
+
#
|
|
305
|
+
# try:
|
|
306
|
+
# update_time = os.path.getmtime(table_path)
|
|
307
|
+
# table_data = pd.read_csv(table_path, sep=";")
|
|
308
|
+
# return table_data.copy(), table_path, update_time
|
|
309
|
+
# except Exception:
|
|
310
|
+
# logger.warning(f"Nao foi possivel carregar a tabela <{table_name}> no formato .csv")
|
|
311
|
+
# return None, None, None
|
|
312
|
+
#
|
|
313
|
+
#def __load_excel(table_name):
|
|
314
|
+
# try:
|
|
315
|
+
# table_path = self.tables_path/f"{table_name}.xlsx"
|
|
316
|
+
# update_time = os.path.getmtime(table_path)
|
|
317
|
+
# # Nao deixar crashar caso nao consiga ler do excel !
|
|
318
|
+
# table_data = pd.read_excel(table_path)
|
|
319
|
+
# logger.warning(f"Tabela {table_name} carregada do arquivo em excel. Limite 1M de linhas.")
|
|
320
|
+
# return table_data.copy(), table_path, update_time
|
|
321
|
+
# except FileNotFoundError:
|
|
322
|
+
# return None, table_path, None
|
|
323
|
+
# Tentando carregar, do mais eficiente pro menos eficiente.
|
|
324
|
+
table_data, table_path, update_time = __load_parquet(table_name)
|
|
325
|
+
#if table_data is None: CSV DESCONTINUADO POR FALTA DE USO
|
|
326
|
+
# table_data, table_path, update_time = __load_csv(table_name)
|
|
327
|
+
#if table_data is None:
|
|
328
|
+
# table_data, table_path, update_time = __load_excel(table_name)
|
|
329
|
+
|
|
330
|
+
#assert(table_data is not None)
|
|
331
|
+
if table_data is None:
|
|
332
|
+
logger.error(f"Nao foi possivel carregar a tabela <{table_name}>.")
|
|
333
|
+
return table_data
|
|
334
|
+
|
|
335
|
+
if index:
|
|
336
|
+
try:
|
|
337
|
+
table_data = table_data.set_index(index_name, drop=True)
|
|
338
|
+
|
|
339
|
+
except Exception:
|
|
340
|
+
logger.error(f"Nao foi possível setar a coluna {index_name} como index para a tabela {table_name}.")
|
|
341
|
+
|
|
342
|
+
table_data = self.__persist_column_formatting(table_data)
|
|
343
|
+
|
|
344
|
+
self.tables_in_use[table_name] = {"table_data" : table_data,
|
|
345
|
+
"table_path" : table_path,
|
|
346
|
+
"update_time" : update_time
|
|
347
|
+
}
|
|
348
|
+
return table_data
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def __load_table_group(self, table_keys):
|
|
352
|
+
|
|
353
|
+
price_tables_loaded_flag = False
|
|
354
|
+
|
|
355
|
+
for table_key in table_keys:
|
|
356
|
+
|
|
357
|
+
index = table_key in ["px_last", "last_all_flds"]
|
|
358
|
+
index_name = "Key" if table_key in ["px_last", "last_all_flds"] else None
|
|
359
|
+
self.__load_table(table_key, index=index, index_name=index_name)
|
|
360
|
+
|
|
361
|
+
if table_key == "px_last":
|
|
362
|
+
self.asset_last_prices = self.get_table(table_key).to_dict("index")
|
|
363
|
+
|
|
364
|
+
elif table_key == "last_all_flds":
|
|
365
|
+
self.last_all_flds = self.get_table(table_key).to_dict("index")
|
|
366
|
+
|
|
367
|
+
elif table_key in ["hist_px_last", "hist_non_bbg_px_last", "hist_vc_px_last", "all_funds_quotas", "custom_funds_quotas", "custom_prices"]:
|
|
368
|
+
price_tables_loaded_flag = True
|
|
369
|
+
table = (self.get_table(table_key)
|
|
370
|
+
.rename(
|
|
371
|
+
columns={"Fund" : "Asset", "Quota" : "Last_Price", "Ticker" : "Asset",
|
|
372
|
+
"Price" : "Last_Price"
|
|
373
|
+
})[["Date","Asset","Last_Price"]]
|
|
374
|
+
)
|
|
375
|
+
if table_key == 'hist_vc_px_last':
|
|
376
|
+
|
|
377
|
+
last_prices = table.groupby("Asset")["Last_Price"].last()
|
|
378
|
+
last_date_df = pd.DataFrame({"Date": [dt.datetime.now()] * len(last_prices)})
|
|
379
|
+
last_date_df["Asset"] = last_prices.index
|
|
380
|
+
last_date_df["Last_Price"] = last_prices.values
|
|
381
|
+
|
|
382
|
+
table = (pd.concat([table, last_date_df]).sort_values(by="Date")
|
|
383
|
+
.set_index("Date").groupby("Asset")
|
|
384
|
+
.apply(lambda g: g[~g.index.duplicated(keep="first")].resample("D").ffill()).reset_index(level=0, drop=True)
|
|
385
|
+
.reset_index())
|
|
386
|
+
|
|
387
|
+
self.price_tables_loaded[table_key] = table
|
|
388
|
+
|
|
389
|
+
if ('px_last' in table_keys) and ('hist_px_last' in self.tables_in_use):
|
|
390
|
+
# Vamos pegar o px_last e colocar na tabela de precos historicos, atualizando ultima ocorrencia
|
|
391
|
+
hist_prices = self.get_table("hist_px_last")
|
|
392
|
+
hist_prices = self.__update_hist_px_last_intraday(hist_prices)
|
|
393
|
+
self.price_tables_loaded["hist_px_last"] = hist_prices
|
|
394
|
+
price_tables_loaded_flag = True
|
|
395
|
+
|
|
396
|
+
if price_tables_loaded_flag: # Somente se a execucao alterou o estado de ´price_tables_loaded
|
|
397
|
+
self.hist_prices_table = pd.concat(self.price_tables_loaded.values()).dropna()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def update(self, update_attempts_limit=8):
|
|
401
|
+
"""
|
|
402
|
+
Atualiza todas as tabelas em uso.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
update_attempts = 0
|
|
406
|
+
update_success = False
|
|
407
|
+
self.price_cache = {} # -> reset da otimizacao da consulta de preco
|
|
408
|
+
|
|
409
|
+
while not update_success and (update_attempts < update_attempts_limit):
|
|
410
|
+
try:
|
|
411
|
+
update_attempts += 1
|
|
412
|
+
for table_key in self.tables_in_use:
|
|
413
|
+
# Verificando se tabela foi criada ou modificada
|
|
414
|
+
if self.__is_table_modified(table_key):
|
|
415
|
+
self.__load_table_group([table_key])
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
update_success = True
|
|
419
|
+
|
|
420
|
+
except PermissionError:
|
|
421
|
+
hist_prices_tables = [] # desconsidera appends feitos no loop nao concluido
|
|
422
|
+
logger.error("Não foi possível carregar as tabelas pois tem algum arquivo aberto.")
|
|
423
|
+
logger.info(f"Tentativas de atualização: {update_attempts} de {update_attempts_limit}")
|
|
424
|
+
time.sleep(30)
|
|
425
|
+
|
|
426
|
+
except:
|
|
427
|
+
logger.error("Não foi possivel carregar as tabelas.")
|
|
428
|
+
logger.info(f"Tentativas de atualização: {update_attempts} de {update_attempts_limit}")
|
|
429
|
+
time.sleep(5*update_attempts)
|
|
430
|
+
|
|
431
|
+
if not update_success:
|
|
432
|
+
logger.critical("Nao foi possivel atualizar os dados. Execução finalizada.")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def text_to_lowercase(self, t):
|
|
436
|
+
"""
|
|
437
|
+
Converte todas as colunas de texto para lowercase
|
|
438
|
+
Args:
|
|
439
|
+
t (dt.DataFrame): pandas DataFrame
|
|
440
|
+
Returns:
|
|
441
|
+
dt.DataFrame
|
|
442
|
+
"""
|
|
443
|
+
try:
|
|
444
|
+
return t.map(lambda x: x.lower().strip() if isinstance(x, str) else x)
|
|
445
|
+
except AttributeError:
|
|
446
|
+
logger.warning("Pendente de atualizacao para o python 3.12.2")
|
|
447
|
+
return t.applymap(lambda x: x.lower().strip() if isinstance(x, str) else x)
|
|
448
|
+
|
|
449
|
+
def get_px_update_time(self):
|
|
450
|
+
""" Informa Horário da ultima atualizacao da base de precos.
|
|
451
|
+
Returns:
|
|
452
|
+
dt.datetime
|
|
453
|
+
"""
|
|
454
|
+
time = self.get_table("px_last")["Query_Time"].max()
|
|
455
|
+
return dt.time(time.hour, time.minute, time.second)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def get_price_key(self, asset_key):
|
|
459
|
+
""" Retorna a chave correta a ser usada para consultar o preco/rentabilidade do ativo."""
|
|
460
|
+
|
|
461
|
+
asset = self.get_table("assets").query("Key == @asset_key").squeeze()
|
|
462
|
+
|
|
463
|
+
if asset["Group"] in ["vc", "luxor"]:
|
|
464
|
+
return asset["Ticker"]
|
|
465
|
+
|
|
466
|
+
# Verificando se ha valor valido de ticker bbg
|
|
467
|
+
if type(asset["Ticker_BBG"]) == type(""):
|
|
468
|
+
|
|
469
|
+
return asset["Ticker_BBG"]
|
|
470
|
+
# caso nao haja, sera usado o ticker
|
|
471
|
+
return asset["Ticker"]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_price(self, ticker, px_date=None, logger_level="trace", dr_adjusted=False,
|
|
475
|
+
currency="local", usdbrl_ticker="bmfxclco curncy", asset_location="us"):
|
|
476
|
+
""" Informa o preço do ativo. Quando px_date nao eh informado, retorna o
|
|
477
|
+
preço mais recente.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
ticker (str): identificador do ativo.
|
|
481
|
+
px_date (dt.date, optional): Data de referencia.
|
|
482
|
+
logger_level (str, optional): Defaults to "trace".
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
float: preço do ativo.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
if pd.isna(ticker): return None
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
currency_factor = 1
|
|
492
|
+
if currency != "local":
|
|
493
|
+
try:
|
|
494
|
+
if asset_location == "bz" and currency == "usd":
|
|
495
|
+
usdbrl = self.get_price(usdbrl_ticker, px_date=px_date)
|
|
496
|
+
currency_factor = 1/usdbrl
|
|
497
|
+
elif asset_location == "us" and currency == "brl":
|
|
498
|
+
usdbrl = self.get_price(usdbrl_ticker, px_date=px_date)
|
|
499
|
+
currency_factor = usdbrl
|
|
500
|
+
except ValueError:
|
|
501
|
+
logger.error(f"Erro ao converter moeda para {currency} para o ticker '{ticker}'.")
|
|
502
|
+
currency_factor = 1
|
|
503
|
+
|
|
504
|
+
ticker = ticker.lower()
|
|
505
|
+
|
|
506
|
+
# dados do usd cupom limpo comecam em abril de 2011. Antes disso vamos pergar usdbrl normalmente
|
|
507
|
+
if (ticker == "bmfxclco curncy") and (px_date is not None) and (px_date < dt.date(2011,4,14) or (px_date is not None and px_date == dt.date.today())) :
|
|
508
|
+
ticker = "usdbrl curncy"
|
|
509
|
+
|
|
510
|
+
px_last_at_date = None
|
|
511
|
+
|
|
512
|
+
cache_key = ticker + str(px_date) # chave unica do preco para essa data
|
|
513
|
+
# se o preco foi consultado recentemente, podemos retorna-lo rapidamente.
|
|
514
|
+
if cache_key in self.price_cache:
|
|
515
|
+
return self.price_cache[cache_key] * currency_factor
|
|
516
|
+
|
|
517
|
+
if (px_date is None) or (px_date>= dt.date.today()):
|
|
518
|
+
# Nesse caso retornamos o mais recente utilizando o dicionario
|
|
519
|
+
|
|
520
|
+
if "px_last" not in self.tables_in_use:
|
|
521
|
+
self.__load_table_group(["px_last"])
|
|
522
|
+
|
|
523
|
+
#if ticker in self.asset_last_prices.keys():
|
|
524
|
+
try:
|
|
525
|
+
px_last_at_date = self.asset_last_prices[ticker]["px_last"]
|
|
526
|
+
self.price_cache[cache_key] = px_last_at_date
|
|
527
|
+
except:
|
|
528
|
+
price_1_tickers = ['fip mission 1.1', 'caixa', 'caixa us']
|
|
529
|
+
if ticker in price_1_tickers :
|
|
530
|
+
px_last_at_date = 1
|
|
531
|
+
self.price_cache[cache_key] = px_last_at_date
|
|
532
|
+
else:
|
|
533
|
+
#if px_last_at_date is None:
|
|
534
|
+
if logger_level == "trace":
|
|
535
|
+
logger.trace(f"Preço nao disponivel para o ticker '{ticker}'. Preço setado para 0.")
|
|
536
|
+
elif logger_level == "info":
|
|
537
|
+
logger.info(f"Preço nao disponivel para o ticker '{ticker}'. Preço setado para 0.")
|
|
538
|
+
else: # logger_level == "erro":
|
|
539
|
+
logger.error(f"Preço nao disponivel para o ticker '{ticker}'. Preço setado para 0.")
|
|
540
|
+
px_last_at_date = 0
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
return px_last_at_date * currency_factor
|
|
544
|
+
|
|
545
|
+
# Vamos olhar em cada tabela de precos procurando pelo ticker informado
|
|
546
|
+
if not self.price_tables_loaded:
|
|
547
|
+
self.__load_table_group(["hist_px_last", "hist_non_bbg_px_last", "hist_vc_px_last",
|
|
548
|
+
"all_funds_quotas", "custom_funds_quotas", "custom_prices"])
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
# Busca otimizada do preco pelo ativo e pela data informada
|
|
553
|
+
#px_last_at_date = (self.hist_prices_table["Last_Price"]
|
|
554
|
+
# .to_numpy()[(
|
|
555
|
+
# (self.hist_prices_table["Date"].dt.date.to_numpy() <= px_date)
|
|
556
|
+
# &
|
|
557
|
+
# (self.hist_prices_table["Asset"].to_numpy() == ticker)
|
|
558
|
+
# )].item(-1)
|
|
559
|
+
# )
|
|
560
|
+
#return px_last_at_date
|
|
561
|
+
|
|
562
|
+
px_last_at_date = self.hist_prices_table.query("Date <= @px_date and Asset == @ticker")
|
|
563
|
+
|
|
564
|
+
return px_last_at_date.tail(1)["Last_Price"].squeeze() * currency_factor if len(px_last_at_date) > 0 else 0
|
|
565
|
+
|
|
566
|
+
except (IndexError , KeyError):
|
|
567
|
+
# Nao achou o ativo em nenhuma das tabelas, retorna 0
|
|
568
|
+
if logger_level == "trace":
|
|
569
|
+
logger.trace(f"Preço nao disponivel para o tikcker '{ticker}'. Preço setado para 0.")
|
|
570
|
+
elif logger_level == "info":
|
|
571
|
+
logger.info(f"Preço nao disponivel para o tikcker '{ticker}'. Preço setado para 0.")
|
|
572
|
+
else: # logger_level == "erro":
|
|
573
|
+
logger.error(f"Preço nao disponivel para o tikcker '{ticker}'. Preço setado para 0.")
|
|
574
|
+
|
|
575
|
+
logger.trace(f"Preço nao disponivel para o tikcker '{ticker}'. Preço setado para 0.")
|
|
576
|
+
self.price_cache[cache_key] = px_last_at_date
|
|
577
|
+
return 0
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def usdbrl_clean_coupon_fix(self, usdbrl_df):
|
|
581
|
+
""" Corrige o problema do ticker bmfxclco curncy nao ter dados intraday.
|
|
582
|
+
Usa a variacao intraday do usdbrl curncy
|
|
583
|
+
Args:
|
|
584
|
+
usdbrl_df (pd.DataFrame): dataframe com precos historicos do bmfxclco curncy
|
|
585
|
+
"""
|
|
586
|
+
max_date = usdbrl_df["Date"].max().date()
|
|
587
|
+
today = dt.date.today()
|
|
588
|
+
if max_date < today:
|
|
589
|
+
# vamos pegar a variacao do usdbrl curncy
|
|
590
|
+
var_usdbrl = self.get_pct_change('usdbrl curncy',
|
|
591
|
+
recent_date=today,
|
|
592
|
+
previous_date=max_date)
|
|
593
|
+
# vamos pegar o last_price em max_date
|
|
594
|
+
last_price = usdbrl_df.query("Date == @max_date")["Last_Price"].squeeze()
|
|
595
|
+
# vamos ajustar com a variacao do usdbrl
|
|
596
|
+
last_price = last_price * (1 + var_usdbrl)
|
|
597
|
+
#vamos colocar na base na data de hoje
|
|
598
|
+
usdbrl_df = pd.concat([usdbrl_df,
|
|
599
|
+
pd.DataFrame({"Date":[today],
|
|
600
|
+
"Asset":['bmfxclco curncy'], "Last_Price":[last_price]})])
|
|
601
|
+
|
|
602
|
+
usdbrl_df["Date"] = pd.to_datetime(usdbrl_df["Date"])
|
|
603
|
+
|
|
604
|
+
return usdbrl_df
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def __update_hist_px_last_intraday(self, hist_prices):
|
|
608
|
+
|
|
609
|
+
tickers = list(hist_prices["Asset"].unique())
|
|
610
|
+
px_last = self.get_table("px_last", index=True, index_name="Key").copy()
|
|
611
|
+
# Selecionando todos os tickers que estao na tabela px_last
|
|
612
|
+
px_last = px_last.query("Key in @tickers").reset_index()
|
|
613
|
+
# Vamos alterar a ultima ocorrencia desses tickers na hist_prices pelos valores na px_last
|
|
614
|
+
# vamos fazer de forma vetorizada]
|
|
615
|
+
hist_prices_columns = list(hist_prices.columns)
|
|
616
|
+
hist_prices = hist_prices.merge(px_last, left_on=["Asset", "Date"],
|
|
617
|
+
right_on=["Key", "last_update_dt"], how="left")
|
|
618
|
+
hist_prices["Last_Price"] = np.where(~hist_prices['px_last'].isna(),
|
|
619
|
+
hist_prices['px_last'],
|
|
620
|
+
hist_prices['Last_Price'])
|
|
621
|
+
hist_prices = hist_prices[hist_prices_columns]
|
|
622
|
+
|
|
623
|
+
return hist_prices
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def __hist_prices_intraday_extensor(self, hist_prices):
|
|
628
|
+
""" Completa a tabela de precos historicos com o preco intraday mais recente.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
hist_prices (pd.DataFrame): dataframe de precos historicos
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
# Verificar cada Asset unico existente na tabela
|
|
635
|
+
assets = hist_prices["Asset"].unique()
|
|
636
|
+
today = dt.date.today()
|
|
637
|
+
updated_dfs = []
|
|
638
|
+
|
|
639
|
+
# vamos fazer cada ativo por vez
|
|
640
|
+
for asset in assets:
|
|
641
|
+
# vamos pegar a tabela de precos do ativo
|
|
642
|
+
asset_prices = hist_prices.query("Asset == @asset")
|
|
643
|
+
# Vamos pegar a data mais recente
|
|
644
|
+
last_date = asset_prices["Date"].max().date()
|
|
645
|
+
if last_date == today:
|
|
646
|
+
# pega preço mais recente
|
|
647
|
+
last_price = self.get_price(asset)
|
|
648
|
+
# atualiza na asset_prices nessa data
|
|
649
|
+
asset_prices.loc[asset_prices["Date"] == last_date, "Last_Price"] = last_price
|
|
650
|
+
updated_dfs.append(asset_prices.copy())
|
|
651
|
+
continue
|
|
652
|
+
# vamos pegar o ultimo preco presente no df
|
|
653
|
+
last_price = asset_prices.query("Date == @last_date")["Last_Price"].squeeze()
|
|
654
|
+
# vamos pegar a variacao ate o momento mais recente
|
|
655
|
+
query_asset = asset
|
|
656
|
+
if asset == 'bmfxclco curncy':
|
|
657
|
+
query_asset = 'usdbrl curncy'
|
|
658
|
+
variation = self.get_pct_change(query_asset, recent_date=today, previous_date=last_date)
|
|
659
|
+
# vamos ajustar o preco com a variacao
|
|
660
|
+
last_price = last_price * (1 + variation)
|
|
661
|
+
# vamos colocar na base na data de hoje
|
|
662
|
+
updated_dfs.append(pd.concat([asset_prices,
|
|
663
|
+
pd.DataFrame({"Date":[today],
|
|
664
|
+
"Asset":[asset], "Last_Price":[last_price]})]))
|
|
665
|
+
|
|
666
|
+
updated_df = pd.concat(updated_dfs)
|
|
667
|
+
updated_df["Date"] = pd.to_datetime(updated_df["Date"])
|
|
668
|
+
|
|
669
|
+
return updated_df
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def get_prices(self, tickers=None, recent_date=dt.date.today(), previous_date=dt.date.today()-dt.timedelta(days=30),
|
|
673
|
+
period=None, currency="local", usdbrl_ticker="bmfxclco curncy", force_continuous_date_range=True,
|
|
674
|
+
holiday_location="all", get_intraday_prices=False, force_month_end=False):
|
|
675
|
+
"""
|
|
676
|
+
Filtra o historico de preços pelos tickers e pelo periodo informado.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
recent_date (dt.date, optional): data de fim. Por padrao, data de hoje.
|
|
680
|
+
previous_date (dt.date, optional): data de inicio. Por padrao, 30 dias antes de hoje.
|
|
681
|
+
tickers (list|set, optional): Lista de tickers que devem ser incluidos, quando existirem.
|
|
682
|
+
period(str): ytd, mtd ou 'xm' onde 'x' é o numero de meses.
|
|
683
|
+
currency(str): codigo de 3 caracteres da moeda ou 'all' para considerar a moeda local de cada ativo.
|
|
684
|
+
period(str): ytd|mtd|'xm' onde 'x' é o numero de meses. Usara como base o parametro 'recent_date'
|
|
685
|
+
holiday_location(str): all|any|us|bz ... ver metodo 'is_holiday'
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
pd.DataFrame: Tabela de precos filtrada e convertida para moeda desejada
|
|
689
|
+
"""
|
|
690
|
+
if period is not None:
|
|
691
|
+
previous_date = self.get_start_period_dt(recent_date, period, holiday_location=holiday_location, force_bday=True,
|
|
692
|
+
force_month_end=force_month_end)
|
|
693
|
+
|
|
694
|
+
if recent_date < previous_date:
|
|
695
|
+
logger.warning("Possivel inversao dos parametros de inicio e fim do periodo.")
|
|
696
|
+
temp = recent_date
|
|
697
|
+
recent_date = previous_date
|
|
698
|
+
previous_date = temp
|
|
699
|
+
|
|
700
|
+
if not self.price_tables_loaded:
|
|
701
|
+
try:
|
|
702
|
+
self.__load_table_group(["hist_px_last", "hist_non_bbg_px_last", "hist_vc_px_last", "all_funds_quotas",
|
|
703
|
+
"custom_funds_quotas", "custom_prices", "px_last"])
|
|
704
|
+
except FileNotFoundError:
|
|
705
|
+
# Vamos tentar sem o custom_funds_quotas, pois pode nao existir ainda.
|
|
706
|
+
self.__load_table_group(["hist_px_last", "hist_non_bbg_px_last", "hist_vc_px_last",
|
|
707
|
+
"all_funds_quotas", "custom_prices", "px_last"])
|
|
708
|
+
|
|
709
|
+
if tickers is None:
|
|
710
|
+
#prices = self.hist_prices_table.query("Date <= @recent_date and Date >= @previous_date")
|
|
711
|
+
# Nesse caso, tickers sera uma lista com todos os ativos da tabela de precos
|
|
712
|
+
tickers = self.hist_prices_table["Asset"].unique()
|
|
713
|
+
|
|
714
|
+
if isinstance(tickers, str):
|
|
715
|
+
tickers = [tickers]
|
|
716
|
+
|
|
717
|
+
tickers = [t.lower() for t in tickers] # padronizando tickers para lowercase
|
|
718
|
+
|
|
719
|
+
prices = self.hist_prices_table.copy()
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
surrogate_previous_date = previous_date - dt.timedelta(days=30)
|
|
723
|
+
surrogate_recent_date = recent_date + dt.timedelta(days=30)
|
|
724
|
+
prices = prices.query("Date <= @surrogate_recent_date and Date >= @surrogate_previous_date\
|
|
725
|
+
and Asset.isin(@tickers)")
|
|
726
|
+
|
|
727
|
+
if force_continuous_date_range:
|
|
728
|
+
# Vamos ajustar as datas logo aqui, caso flag esteja ativa
|
|
729
|
+
prices = prices.set_index("Date").groupby("Asset")\
|
|
730
|
+
.resample("D").last().ffill()\
|
|
731
|
+
.reset_index(level=0, drop=True).reset_index()
|
|
732
|
+
|
|
733
|
+
if get_intraday_prices:
|
|
734
|
+
prices = self.__hist_prices_intraday_extensor(prices)
|
|
735
|
+
|
|
736
|
+
if currency != "local":
|
|
737
|
+
|
|
738
|
+
# TODO: Resolver problema de consistencia com ticker e ticker_bbg
|
|
739
|
+
|
|
740
|
+
assets= self.get_table("assets").copy().query("Type != 'ações_bdr' and Asset != 'spx eagle'")
|
|
741
|
+
|
|
742
|
+
ticker_map = assets.query("~Ticker_BBG.isna() and Ticker_BBG != Ticker")[["Ticker", "Ticker_BBG"]].set_index("Ticker").to_dict("index")
|
|
743
|
+
assets["Ticker"] = assets["Ticker"].apply(lambda x: ticker_map[x]["Ticker_BBG"] if x in ticker_map.keys() else x)
|
|
744
|
+
|
|
745
|
+
prices = pd.merge(assets[["Ticker", "Location"]], prices, left_on="Ticker", right_on="Asset")[["Date", "Asset", "Last_Price", "Location"]]
|
|
746
|
+
currency_map = {"bz":"brl", "us":"usd", "cn":"cad", "eur":"eur"}
|
|
747
|
+
prices["Location"] = prices["Location"].apply(lambda x: currency_map.get(x, 'not found'))
|
|
748
|
+
|
|
749
|
+
prices_by_location = []
|
|
750
|
+
iter_prices = prices.groupby("Location")
|
|
751
|
+
|
|
752
|
+
for p in iter_prices:
|
|
753
|
+
if p[0] == 'not found':
|
|
754
|
+
assets_not_found = list(p[1]["Asset"].unique())
|
|
755
|
+
logger.warning(f"currency_map nao suporta Location dos ativos {assets_not_found}.")
|
|
756
|
+
continue
|
|
757
|
+
price_currency = p[0]
|
|
758
|
+
converted_prices = self.convert_currency(p[1][list(set(p[1].columns) - {"Location"})],
|
|
759
|
+
price_currency=price_currency, dest_currency=currency,
|
|
760
|
+
usdbrl_ticker=usdbrl_ticker, force_continuous_date_range=force_continuous_date_range,
|
|
761
|
+
holiday_location=holiday_location, get_intraday_prices=get_intraday_prices,
|
|
762
|
+
force_month_end=force_month_end)
|
|
763
|
+
if not converted_prices.empty:
|
|
764
|
+
prices_by_location.append(converted_prices)
|
|
765
|
+
|
|
766
|
+
# Tratando o caso de nao haver conversao para nenhum dos tickers
|
|
767
|
+
if len(prices_by_location) > 0:
|
|
768
|
+
prices = pd.concat(prices_by_location)[["Date", "Asset", "Last_Price"]].sort_values(by=["Asset", "Date"])
|
|
769
|
+
else:
|
|
770
|
+
prices = pd.DataFrame({}, columns=["Date", "Asset", "Last_Price"])
|
|
771
|
+
|
|
772
|
+
# Finalmente, vamos seguir filtrando pelo periodo desejado.
|
|
773
|
+
prices = prices.query("Date <= @recent_date and Date >= @previous_date and Asset.isin(@tickers)")
|
|
774
|
+
|
|
775
|
+
#if adjust_bmfxlcoc and ('bmfxclco curncy' in tickers) and (recent_date == dt.date.today()):
|
|
776
|
+
# max_date = prices.query("Asset == 'bmfxclco curncy'")["Date"].max()
|
|
777
|
+
# if max_date < dt.date.today(): # vamos colocar mais um dia usando usdbrl curncy
|
|
778
|
+
# var_usdbrl1d = self.get_pct_change('usdbrl curncy',
|
|
779
|
+
# recent_date=dt.date.today(),
|
|
780
|
+
# previous_date=max_date)
|
|
781
|
+
# # vamos pegar o last_price em max_date
|
|
782
|
+
# last_price = self.get_price('bmfxclco curncy', px_date=max_date)
|
|
783
|
+
# # vamos ajustar com a variacao do usdbrl
|
|
784
|
+
# last_price = last_price * (1 + var_usdbrl1d)
|
|
785
|
+
# #vamos colocar na base na data de hoje
|
|
786
|
+
# prices = pd.concat([prices,
|
|
787
|
+
# pd.DataFrame({"Date":[dt.date.today()],
|
|
788
|
+
# "Asset":['bmfxclco curncy'], "Last_Price":[last_price]})])
|
|
789
|
+
|
|
790
|
+
return prices
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def convert_currency(self, prices, price_currency, dest_currency, usdbrl_ticker="bmfxclco curncy",
|
|
794
|
+
force_continuous_date_range=True, holiday_location="all",
|
|
795
|
+
get_intraday_prices=False, force_month_end=False):
|
|
796
|
+
|
|
797
|
+
if price_currency == dest_currency: return prices
|
|
798
|
+
|
|
799
|
+
convertion_rule = {"brl_usd" : {"currency_ticker":usdbrl_ticker,
|
|
800
|
+
"operation":"divide"},
|
|
801
|
+
"usd_brl" : {"currency_ticker":usdbrl_ticker,
|
|
802
|
+
"operation":"multiply"},
|
|
803
|
+
"eur_usd" : {"currency_ticker":"usdeur curncy",
|
|
804
|
+
"operation":"divide"},
|
|
805
|
+
"usd_eur" : {"currency_ticker":"usdeur curncy",
|
|
806
|
+
"operation":"multiply"},
|
|
807
|
+
"usd_cad" : {"currency_ticker":"cad curncy",
|
|
808
|
+
"operation":"multiply"},
|
|
809
|
+
"cad_usd" : {"currency_ticker":"cad curncy",
|
|
810
|
+
"operation":"divide"},
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
convertion_data = convertion_rule.get(price_currency+"_"+dest_currency, None)
|
|
814
|
+
if convertion_data is None:
|
|
815
|
+
logger.error(f"Conversao de moeda nao disponivel para {price_currency} -> {dest_currency}.")
|
|
816
|
+
prices = pd.DataFrame({}, columns=prices.columns)
|
|
817
|
+
return prices
|
|
818
|
+
convertion_ticker = convertion_data["currency_ticker"]
|
|
819
|
+
convertion_operation = convertion_data["operation"]
|
|
820
|
+
|
|
821
|
+
previous_date = prices["Date"].min()
|
|
822
|
+
recent_date = prices["Date"].max()
|
|
823
|
+
|
|
824
|
+
currencies = self.get_prices(convertion_ticker, previous_date=previous_date, recent_date=recent_date,
|
|
825
|
+
force_continuous_date_range=force_continuous_date_range,
|
|
826
|
+
holiday_location=holiday_location, get_intraday_prices=get_intraday_prices,
|
|
827
|
+
force_month_end=force_month_end).copy()
|
|
828
|
+
if convertion_operation == "divide":
|
|
829
|
+
currencies["Last_Price"] = 1/currencies["Last_Price"]
|
|
830
|
+
|
|
831
|
+
prices = pd.merge(prices, currencies[["Date", "Last_Price"]].rename(columns={"Last_Price":"Currency"}), on="Date")[list(prices.columns) + ["Currency"]]
|
|
832
|
+
|
|
833
|
+
prices["Last_Price"] = prices["Last_Price"] * prices["Currency"]
|
|
834
|
+
|
|
835
|
+
return prices[list(set(prices.columns) - {"Currency"}) ]
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def get_data(self, ticker, flds, data_date=None, logger_level="trace"):
|
|
839
|
+
|
|
840
|
+
flds = flds.lower().replace(" ","_")
|
|
841
|
+
|
|
842
|
+
if flds == "px_last":
|
|
843
|
+
return self.get_price(ticker, px_date=data_date, logger_level=logger_level)
|
|
844
|
+
|
|
845
|
+
if data_date is None:
|
|
846
|
+
# Vamos buscar o dado mais recente
|
|
847
|
+
data_value = None
|
|
848
|
+
try:
|
|
849
|
+
data_value = self.last_all_flds[ticker+"_"+flds]["Value"]
|
|
850
|
+
except AttributeError:
|
|
851
|
+
self.__load_table_group(["last_all_flds"])
|
|
852
|
+
try:
|
|
853
|
+
data_value = self.last_all_flds[ticker+"_"+flds]["Value"]
|
|
854
|
+
except KeyError:
|
|
855
|
+
if logger_level == "trace":
|
|
856
|
+
logger.trace(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
857
|
+
elif logger_level == "info":
|
|
858
|
+
logger.info(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
859
|
+
else: # logger_level == "erro":
|
|
860
|
+
logger.error(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
861
|
+
return None
|
|
862
|
+
except KeyError:
|
|
863
|
+
if logger_level == "trace":
|
|
864
|
+
logger.trace(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
865
|
+
elif logger_level == "info":
|
|
866
|
+
logger.info(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
867
|
+
else: # logger_level == "erro":
|
|
868
|
+
logger.error(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
869
|
+
return None
|
|
870
|
+
|
|
871
|
+
# Formatando o valor retornado
|
|
872
|
+
data_type = self.get_table("field_map").query("Field == @flds")["Value_Type"].squeeze()
|
|
873
|
+
|
|
874
|
+
if data_type == 'numerical':
|
|
875
|
+
return float(data_value)
|
|
876
|
+
if data_type == "date":
|
|
877
|
+
return dt.datetime.fromtimestamp(float(data_value))
|
|
878
|
+
if data_type != "text":
|
|
879
|
+
logger.warning(f"field '{flds}' nao foi cadastrado na tabela field_map.")
|
|
880
|
+
return data_value
|
|
881
|
+
|
|
882
|
+
# Buscamos por dado numa data especifica.
|
|
883
|
+
hist_all_flds = self.get_table("hist_all_flds")
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
data = hist_all_flds.query("Date <= @data_date and Field == @flds and Ticker == @ticker")["Value"]
|
|
887
|
+
|
|
888
|
+
if len(data) > 0:
|
|
889
|
+
return data.tail(1).squeeze()
|
|
890
|
+
except KeyError:
|
|
891
|
+
logger.error(f"Dado de {flds} nao disponivel para o ticker '{ticker}'.")
|
|
892
|
+
# Nenhum dado encontrado para a data informada
|
|
893
|
+
return None
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def get_bdr_size(self, ticker):
|
|
897
|
+
if ticker == 'mcor34 bz equity':
|
|
898
|
+
return 4 #TODO remover quando atualizacao da table for reestabelecida
|
|
899
|
+
sizes = self.get_table("bdr_sizes", index=True, index_name="Ticker")
|
|
900
|
+
|
|
901
|
+
return sizes.loc[ticker.lower()].squeeze()
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def get_cash_movements(self, fund_name, ref_date):
|
|
905
|
+
|
|
906
|
+
cash_movements = self.get_table("cash_movements")
|
|
907
|
+
#cash_movements["Settlement Date"] = cash_movements["Settlement Date"].dt.date
|
|
908
|
+
#cash_movements["Date"] = pd.to_datetime(cash_movements["Date"]).dt.date #Conversao para Date só funcionou dessa maneira
|
|
909
|
+
cash_movements = cash_movements.query("(Fund == @fund_name) and (Date > @ref_date)")[["Type", "Volume"]]
|
|
910
|
+
#cash_movements = cash_movements.loc[ ((cash_movements["Fund"] == fund_name) & (cash_movements["Date"] > ref_date)) ] [["Type", "Volume"]]
|
|
911
|
+
#cash_movements.to_excel("temmmp.xlsx")
|
|
912
|
+
cash_movements = dict(cash_movements.groupby("Type")["Volume"].sum())
|
|
913
|
+
|
|
914
|
+
return cash_movements
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def get_avg_price(self, fund_name, asset_key, date=None):
|
|
918
|
+
|
|
919
|
+
if date is None:
|
|
920
|
+
date = dt.date.today()
|
|
921
|
+
|
|
922
|
+
positions = self.get_table("hist_positions").query("Fund == @fund_name and Asset_ID == @asset_key and Date <= @date")
|
|
923
|
+
|
|
924
|
+
if len(positions) > 0:
|
|
925
|
+
return positions.tail(1).squeeze()["Avg_price"]
|
|
926
|
+
|
|
927
|
+
return 0
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def get_term_debt(self, fund_name):
|
|
931
|
+
|
|
932
|
+
term_debt = self.get_table("last_positions")
|
|
933
|
+
term_debt = term_debt.loc [((term_debt["Asset"].str.contains("term debt"))&(term_debt["Fund"]==fund_name))][["#", "Avg_price"]]
|
|
934
|
+
total_debt = -(term_debt["#"] * term_debt["Avg_price"]).sum()
|
|
935
|
+
|
|
936
|
+
return total_debt
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def is_holiday(self, date, location):
|
|
940
|
+
"""
|
|
941
|
+
Args:
|
|
942
|
+
date (dt.date):
|
|
943
|
+
location (str):
|
|
944
|
+
'any'-> se é feriado em qualquer um dos lugares cadastrados\n
|
|
945
|
+
'all' -> se é feriado em todas as localidades da tabela\n
|
|
946
|
+
'us', 'bz', (...) -> para local especifico, com codigo presente na tabela\n
|
|
947
|
+
Returns:
|
|
948
|
+
bool: se é ou nao feriado
|
|
949
|
+
"""
|
|
950
|
+
holidays = self.get_table("holidays")
|
|
951
|
+
|
|
952
|
+
if location != "any" and location != "all":
|
|
953
|
+
holidays = holidays.loc[holidays["Location"] == location]
|
|
954
|
+
if location == "all":
|
|
955
|
+
n_locations = len(holidays["Location"].unique())
|
|
956
|
+
holidays = (holidays["Date"]
|
|
957
|
+
.value_counts().reset_index()
|
|
958
|
+
.query("count == @n_locations")
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
return date in set(holidays["Date"].dt.date)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def get_bday_offset(self, date, offset=1, location="bz"):
|
|
965
|
+
""" Retorna dia util com offset dias pra frente ou pra tras.\n
|
|
966
|
+
location (str):
|
|
967
|
+
'any'-> se é feriado em qualquer um dos lugares cadastrados\n
|
|
968
|
+
'all' -> se é feriado em todas as localidades da tabela\n
|
|
969
|
+
'us', 'bz', (...) -> para local especifico, com codigo presente na tabela\n
|
|
970
|
+
"""
|
|
971
|
+
offset_direction = 0
|
|
972
|
+
i = 1
|
|
973
|
+
if offset != 0:
|
|
974
|
+
offset_direction = offset/abs(offset)
|
|
975
|
+
i = abs(offset)
|
|
976
|
+
|
|
977
|
+
while i > 0 :
|
|
978
|
+
# Pegamos proxima data
|
|
979
|
+
date = date + dt.timedelta(days=offset_direction)
|
|
980
|
+
# Verificamos se eh dia util. Se for, contabilizamos.
|
|
981
|
+
if (date.weekday() < 5) and (not self.is_holiday(date, location) ):
|
|
982
|
+
i -= 1
|
|
983
|
+
elif offset == 0:
|
|
984
|
+
offset_direction = -1
|
|
985
|
+
|
|
986
|
+
return date
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def get_bdays_count(self, recent_date, previous_date, location="bz"):
|
|
990
|
+
"""
|
|
991
|
+
Calcula quantos dias uteis existem entre recent_date e previous_date
|
|
992
|
+
(Exclusivo, ou seja, recent_date e previous_date nao sao contados)
|
|
993
|
+
"""
|
|
994
|
+
counter = 0
|
|
995
|
+
iter_date = recent_date - dt.timedelta(days=1) # primeiro dia nao eh contado
|
|
996
|
+
|
|
997
|
+
while iter_date > previous_date:
|
|
998
|
+
|
|
999
|
+
if (iter_date.weekday() < 5) and (not self.is_holiday(iter_date, location) ):
|
|
1000
|
+
counter += 1
|
|
1001
|
+
iter_date -= dt.timedelta(days=1)
|
|
1002
|
+
|
|
1003
|
+
return counter
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def get_month_end(self, ref_date):
|
|
1007
|
+
|
|
1008
|
+
ref_date = dt.date(ref_date.year, ref_date.month, 20) + dt.timedelta(days=20)
|
|
1009
|
+
return dt.date(ref_date.year, ref_date.month, 1) - dt.timedelta(days=1)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def get_start_period_dt(self, ref_date, period, force_bday=False, holiday_location="all",
|
|
1013
|
+
force_month_end=False):
|
|
1014
|
+
""" A partir da data informada e do periodo, retorna a data de inicio do periodo.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
ref_date (dt.date): A data de referencia
|
|
1018
|
+
period (str): O periodo em questao dado em meses, ou ytd ou mtd. Ex.: '12m', '6m', 'ytd', 'mtd', '24m'
|
|
1019
|
+
force_bday (bool): Determina se a data retorna devera ser dia util\n
|
|
1020
|
+
holiday_location (str): \n
|
|
1021
|
+
"bz","us" -> considerar feriados num local especifico\n
|
|
1022
|
+
"all" -> considerar feriados globais apenas\n
|
|
1023
|
+
"any" -> considerar feriado em qualquer localidade (dentre bz e us)
|
|
1024
|
+
"""
|
|
1025
|
+
period = period.lower()
|
|
1026
|
+
start_date = None
|
|
1027
|
+
if period[-1] == "m":
|
|
1028
|
+
|
|
1029
|
+
n_months = int(period.split("m")[0])
|
|
1030
|
+
|
|
1031
|
+
if force_month_end:
|
|
1032
|
+
start_date = ref_date - pd.DateOffset(months=n_months)
|
|
1033
|
+
start_date = start_date.date()
|
|
1034
|
+
start_date = self.get_month_end(start_date)
|
|
1035
|
+
else:
|
|
1036
|
+
|
|
1037
|
+
is_leap_date = ((ref_date.month == 2) and (ref_date.day == 29))
|
|
1038
|
+
|
|
1039
|
+
if is_leap_date:
|
|
1040
|
+
ref_date = ref_date-dt.timedelta(days=1)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
year_offset = n_months//12 # obtendo o numero de anos inteiros no periodo informado
|
|
1044
|
+
month_offset = n_months%12 # obtendo o numero de anos parciais
|
|
1045
|
+
|
|
1046
|
+
start_date = dt.date(ref_date.year-year_offset, ref_date.month, ref_date.day)
|
|
1047
|
+
start_date = start_date - month_offset * dt.timedelta(days=30)
|
|
1048
|
+
|
|
1049
|
+
if is_leap_date:
|
|
1050
|
+
start_date = start_date + dt.timedelta(days=10) # forcando avanco ao mes seguinte
|
|
1051
|
+
# Retornando ao ultimo dia do mes anterior
|
|
1052
|
+
start_date = dt.date(start_date.year, start_date.month, 1)-dt.timedelta(days=1)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
elif period == "ytd":
|
|
1056
|
+
start_date = dt.date(ref_date.year-1, 12, 31)
|
|
1057
|
+
|
|
1058
|
+
elif period == "mtd":
|
|
1059
|
+
start_date = dt.date(ref_date.year, ref_date.month , 1) - dt.timedelta(days=1)
|
|
1060
|
+
|
|
1061
|
+
# A partir de uma data pegar o trimestre anterior
|
|
1062
|
+
elif period == 'qtr':
|
|
1063
|
+
start_date = ref_date - pd.DateOffset(months=3)
|
|
1064
|
+
start_date = start_date.date()
|
|
1065
|
+
start_date = self.get_month_end(start_date)
|
|
1066
|
+
|
|
1067
|
+
if force_bday:
|
|
1068
|
+
start_date = self.get_bday_offset(start_date, offset=0, location=holiday_location)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
return start_date
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def __calculate_benchmark_spxt_cash(self, previous_date, recent_date, prop_spxt=0.8, prop_cash=0.2,
|
|
1075
|
+
cash_ticker="jpmutcc lx equity", spx_ticker="spxt index", usdbrl_ticker="bmfxclco curncy"):
|
|
1076
|
+
|
|
1077
|
+
df = self.get_benchmark_spx_cash(previous_date=previous_date-dt.timedelta(days=40), recent_date=recent_date,
|
|
1078
|
+
spx_ticker=spx_ticker, prop_spx=prop_spxt, prop_cash=prop_cash, cash_ticker=cash_ticker,
|
|
1079
|
+
usdbrl_ticker=usdbrl_ticker).reset_index()
|
|
1080
|
+
#"inx index"
|
|
1081
|
+
previous_value = df.query("Date <= @previous_date").tail(1)["Benchmark"].squeeze()
|
|
1082
|
+
recent_value = df.query("Date <= @recent_date").tail(1)["Benchmark"].squeeze()
|
|
1083
|
+
|
|
1084
|
+
return (recent_value/previous_value) -1
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def get_benchmark_spx_cash(self, previous_date:dt.date, recent_date:dt.date, prop_spx=0.8, prop_cash=0.2, currency="usd",
|
|
1088
|
+
usdbrl_ticker="bmfxclco curncy", cash_ticker="jpmutcc lx equity", spx_ticker="spxt index"):
|
|
1089
|
+
"""
|
|
1090
|
+
Obtem a serie historica do benchmark luxor a partir do indice s&p e caixa nas proporcoes desejadas.
|
|
1091
|
+
Args:
|
|
1092
|
+
previous_date (datetime.date): Data inicial desejada
|
|
1093
|
+
recent_date (datetime.date): Data final desejada
|
|
1094
|
+
prop_spx (float, optional): Proporcao s&p. Defaults to 0.8.
|
|
1095
|
+
prop_cash (float, optional): Proporcao caixa. Defaults to 0.2.
|
|
1096
|
+
currency (str, optional): Moeda desejada ('usd','brl'). Defaults to "usd".
|
|
1097
|
+
usdbrl_ticker (str, optional): Ticker do usdbrl para conversao. Defaults to "bmfxclco curncy".
|
|
1098
|
+
cash_ticker (str, optional): Ticker do ativo usado como caixa. Defaults to "jpmutcc lx equity".
|
|
1099
|
+
spx_ticker (str, optional): Ticker do ativo usado como s&p. Defaults to "spxt index".
|
|
1100
|
+
Returns:
|
|
1101
|
+
pandas.DataFrame: DataFrame contendo coluna Benchmark e Datas como index.
|
|
1102
|
+
"""
|
|
1103
|
+
complementary_previous_date=previous_date
|
|
1104
|
+
|
|
1105
|
+
if previous_date < dt.date(2018,12,31) and cash_ticker == 'jpmutcc lx equity':
|
|
1106
|
+
#logger.warning(f"Nao ha datas anteriores a {dt.date(2018,12,31)} para o JPMUTCC. Sera usada essa.")
|
|
1107
|
+
previous_date = dt.date(2018,12,31)
|
|
1108
|
+
|
|
1109
|
+
if previous_date < dt.date(2020,3,2) and cash_ticker == 'sofrindx index':
|
|
1110
|
+
#logger.warning(f"Nao ha datas anteriores a {dt.date(2018,12,31)} para o JPMUTCC. Sera usada essa.")
|
|
1111
|
+
previous_date = dt.date(2020,3,2)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
data = self.get_prices(tickers=[spx_ticker, cash_ticker], previous_date=previous_date, currency=currency,recent_date=recent_date,
|
|
1116
|
+
usdbrl_ticker=usdbrl_ticker)
|
|
1117
|
+
|
|
1118
|
+
if recent_date == dt.date.today():
|
|
1119
|
+
d = dt.datetime(recent_date.year, recent_date.month, recent_date.day)
|
|
1120
|
+
# Precisamos ajustar o ultimo preco para o mais recente
|
|
1121
|
+
last_prices = pd.DataFrame({"Date" : [d, d],
|
|
1122
|
+
"Asset" : ["spxt index",cash_ticker],
|
|
1123
|
+
"Last_Price" : [self.get_price("spxt index"), self.get_price(cash_ticker)]
|
|
1124
|
+
})
|
|
1125
|
+
data = pd.concat([data.query("Date < @d").copy(), last_prices])
|
|
1126
|
+
|
|
1127
|
+
if complementary_previous_date != previous_date:
|
|
1128
|
+
# Entrar aqui significa que o periodo precisou ser ajustado, pois eh anterior a existencia do indice usado para o caixa
|
|
1129
|
+
# Nesse caso, vamos pegar todo o periodo que faltou e considerar retorno diario do FED FUND
|
|
1130
|
+
|
|
1131
|
+
# Primeiro, obtemos o periodo completo do s&p
|
|
1132
|
+
data_spx = self.get_prices(tickers=[spx_ticker], previous_date=complementary_previous_date,
|
|
1133
|
+
recent_date=recent_date, currency=currency, usdbrl_ticker=usdbrl_ticker)
|
|
1134
|
+
# Em seguida, vamos pegar o parcial do jpmutcc e completar com o treasury,
|
|
1135
|
+
# Para isso, sera necessario criar uma serie normalizada a partir dos retornos diarios
|
|
1136
|
+
data_treasury = data.query("Asset == @cash_ticker").copy().set_index(["Date","Asset"]).pct_change().reset_index().dropna()
|
|
1137
|
+
cash_initial_date = min(data_treasury["Date"])
|
|
1138
|
+
complementary_treasury = self.get_prices(tickers=["fedl01 index"], previous_date=complementary_previous_date,
|
|
1139
|
+
recent_date=recent_date, currency=currency, usdbrl_ticker=usdbrl_ticker)
|
|
1140
|
+
complementary_treasury["Last_Price"] = (1 + complementary_treasury["Last_Price"]/100) ** (1/360)-1
|
|
1141
|
+
complementary_treasury = complementary_treasury.query("Date < @cash_initial_date").copy()
|
|
1142
|
+
data_treasury = pd.concat([complementary_treasury, data_treasury])
|
|
1143
|
+
data_treasury["Asset"] = cash_ticker
|
|
1144
|
+
data_treasury["Last_Price"] = (data_treasury["Last_Price"] + 1).cumprod()
|
|
1145
|
+
data = pd.concat([data_spx, data_treasury])
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
# Gerando tabela de granularidade mensal, com as cotas mensais do indicador
|
|
1150
|
+
df = data.pivot(columns="Asset", index="Date",values="Last_Price").ffill().bfill().reset_index()
|
|
1151
|
+
df = df.groupby(df['Date'].dt.to_period('M')).last().reset_index(drop=True).set_index("Date")
|
|
1152
|
+
df = df.pct_change().fillna(0).reset_index()
|
|
1153
|
+
df["Benchmark"] = (df[spx_ticker] * prop_spx + df[cash_ticker] * prop_cash) + 1
|
|
1154
|
+
# Salvando a base mensal num novo df
|
|
1155
|
+
df_benchmark = df[["Date", "Benchmark"]].set_index("Date").cumprod()
|
|
1156
|
+
|
|
1157
|
+
# Gerando base de granularidade diaria com o fator de retorno mtd com 80% spxt e 20% jpmutcc
|
|
1158
|
+
df = data.set_index("Date")
|
|
1159
|
+
df["Last_Price"] = df.groupby(["Asset"]).pct_change().fillna(0)+1
|
|
1160
|
+
df = df.rename(columns={"Last_Price":"Pct_Change"})
|
|
1161
|
+
|
|
1162
|
+
df = df.pivot(columns="Asset", values="Pct_Change")
|
|
1163
|
+
|
|
1164
|
+
df[cash_ticker] = df[cash_ticker].fillna(1) #df[cash_ticker].ffill() # como tende a ser constante, podemos estimar os valores na
|
|
1165
|
+
df[spx_ticker] = df[spx_ticker].fillna(1) # para o s&p por ser variavel, preencheremos com 0 sempre
|
|
1166
|
+
|
|
1167
|
+
df["Year"] = df.index.year
|
|
1168
|
+
df["Month"] = df.index.month
|
|
1169
|
+
# Acumulando para encontrar o retorno MTD
|
|
1170
|
+
df_mtd_w = df.groupby(["Year", "Month"]).cumprod()
|
|
1171
|
+
# Ponderando para encontrar o benchmark MTD 80/20
|
|
1172
|
+
df_mtd_w["MTD"] = df_mtd_w[cash_ticker] * prop_cash + df_mtd_w[spx_ticker] * prop_spx
|
|
1173
|
+
|
|
1174
|
+
# Agora que temos o fator MTD granularidade diaria e a cota mensal
|
|
1175
|
+
# Vamos colocar a cota mensal numa coluna da tabela de granulariade diaria
|
|
1176
|
+
# Primeiro, precisamos de uma juncao que compreenda ano e mes
|
|
1177
|
+
df_mtd_w["Year"] = df_mtd_w.index.year
|
|
1178
|
+
df_mtd_w["Month"] = df_mtd_w.index.month
|
|
1179
|
+
df_benchmark["Year"] = df_benchmark.index.year
|
|
1180
|
+
df_benchmark["Month"] = df_benchmark.index.month
|
|
1181
|
+
df_benchmark = df_benchmark.rename(columns={"Benchmark":"Previous_Month_Acc"})
|
|
1182
|
+
|
|
1183
|
+
# shiftando para que seja sempre a rentabilidade acumulada ate o mes anterior.
|
|
1184
|
+
# Essa operacao tambem ira descartar qualquer acumulo feito numa data que nao for fim de mes.
|
|
1185
|
+
df_benchmark["Previous_Month_Acc"] = df_benchmark["Previous_Month_Acc"].shift(1).ffill().bfill()
|
|
1186
|
+
|
|
1187
|
+
# Preenchemos entao uma coluna com a rentabilidade de cada mes anterior
|
|
1188
|
+
df = df_mtd_w.reset_index().merge(df_benchmark, on=["Month", "Year"], how="left")
|
|
1189
|
+
|
|
1190
|
+
# Finalmente, o benchmark sera obtido atraves do acumulado anterior acumulado com o MTD atual
|
|
1191
|
+
df["Benchmark"] = df["Previous_Month_Acc"] * df["MTD"]
|
|
1192
|
+
|
|
1193
|
+
return df[["Date", "Benchmark"]].set_index("Date")
|
|
1194
|
+
|
|
1195
|
+
def get_quota_adjusted_by_amortization(self, fund_name, quota, date):
|
|
1196
|
+
|
|
1197
|
+
amortizations = { # Amortizacao Bruta / PL antes da amortizacao
|
|
1198
|
+
"maratona" : [{"event_date" : dt.date(2024,1,4), "amortization_ratio" : 2_517_404.21/24_165_260.40},
|
|
1199
|
+
{"event_date" : dt.date(2025,1,8), "amortization_ratio" : 950_000/27_633_373.46},
|
|
1200
|
+
],
|
|
1201
|
+
}
|
|
1202
|
+
# Verificando se ha amortizacoes para o ativo e aplicando.
|
|
1203
|
+
if fund_name in amortizations.keys():
|
|
1204
|
+
amortization = amortizations[fund_name]
|
|
1205
|
+
for amortization in amortization:
|
|
1206
|
+
if date < amortization["event_date"]:
|
|
1207
|
+
quota = quota * (1-amortization["amortization_ratio"])
|
|
1208
|
+
return quota
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def get_pct_change(self, ticker, recent_date= dt.date.today() , previous_date=dt.date.today()-dt.timedelta(days=1),
|
|
1212
|
+
period=None, holiday_location="all", adjust_amortization=True, force_month_end=False):
|
|
1213
|
+
|
|
1214
|
+
if period is not None:
|
|
1215
|
+
previous_date = self.get_start_period_dt(previous_date, period=period, holiday_location=holiday_location,
|
|
1216
|
+
force_month_end=force_month_end)
|
|
1217
|
+
|
|
1218
|
+
if recent_date < previous_date:
|
|
1219
|
+
logger.warning("Possivel inversao dos parametros de inicio e fim do periodo.")
|
|
1220
|
+
temp = recent_date
|
|
1221
|
+
recent_date = previous_date
|
|
1222
|
+
previous_date = temp
|
|
1223
|
+
|
|
1224
|
+
if ticker == 'benchmark luxor spx cash' and recent_date == dt.date.today():
|
|
1225
|
+
return self.__calculate_benchmark_spxt_cash(previous_date, recent_date, cash_ticker="sofrindx index")
|
|
1226
|
+
elif ticker == 'benchmark luxor sp500 85/15 cash' and recent_date == dt.date.today():
|
|
1227
|
+
return self.__calculate_benchmark_spxt_cash(previous_date, recent_date, prop_spxt=0.85, prop_cash=0.15,
|
|
1228
|
+
cash_ticker='sofrindx index')
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
last_price = self.get_price(ticker, px_date=recent_date)
|
|
1232
|
+
|
|
1233
|
+
if last_price == 0 or last_price is None:
|
|
1234
|
+
return 0
|
|
1235
|
+
|
|
1236
|
+
previous_price = self.get_price(ticker, px_date=previous_date)
|
|
1237
|
+
|
|
1238
|
+
try:
|
|
1239
|
+
if (previous_price == 0) or (previous_date is None) or (previous_price is None):
|
|
1240
|
+
return 0
|
|
1241
|
+
except ValueError:
|
|
1242
|
+
logger.error(f"ValueError:\nticker:{ticker} previous_price: {previous_price} previous_date:{previous_date}")
|
|
1243
|
+
|
|
1244
|
+
if adjust_amortization:
|
|
1245
|
+
last_price = self.get_quota_adjusted_by_amortization(ticker, last_price, recent_date)
|
|
1246
|
+
previous_price = self.get_quota_adjusted_by_amortization(ticker, previous_price, previous_date)
|
|
1247
|
+
|
|
1248
|
+
return last_price/previous_price-1
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def get_pct_changes(self, tickers=None, recent_date=dt.date.today(),
|
|
1252
|
+
previous_date=dt.date.today()-dt.timedelta(days=1), period=None, currency="local",
|
|
1253
|
+
usdbrl_ticker="bmfxclco curncy", adjust_amortization=False, force_month_end=False):
|
|
1254
|
+
#TODO -> garantir que a tabela de precos esta sendo atualizada pelos precos no intraday
|
|
1255
|
+
if adjust_amortization:
|
|
1256
|
+
logger.critical("Ajuste de amortizacao ainda NAO implementado para esse metodo.")
|
|
1257
|
+
# Aproveitando a get_prices, para realizar as filtragens
|
|
1258
|
+
pct_changes = self.get_prices(tickers=tickers, recent_date=recent_date,
|
|
1259
|
+
previous_date=previous_date, currency=currency,
|
|
1260
|
+
period=period, usdbrl_ticker=usdbrl_ticker,
|
|
1261
|
+
force_month_end=force_month_end, get_intraday_prices=True
|
|
1262
|
+
).set_index("Date")
|
|
1263
|
+
if pct_changes.empty:
|
|
1264
|
+
return pd.DataFrame(columns=["Asset", "Pct_Change"]).set_index("Asset")
|
|
1265
|
+
|
|
1266
|
+
pct_changes["Pct_Change"] = pct_changes.groupby("Asset").pct_change().fillna(0)+1
|
|
1267
|
+
pct_changes = pct_changes[["Asset", "Pct_Change"]]
|
|
1268
|
+
pct_changes["Pct_Change"] = pct_changes.groupby("Asset").cumprod()-1
|
|
1269
|
+
pct_changes = pct_changes.groupby("Asset").last()
|
|
1270
|
+
|
|
1271
|
+
return pct_changes
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def __calculate_pct_table(self):
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
self.hist_pct_change = self.hist_prices_table.copy().set_index("Date")
|
|
1279
|
+
self.hist_pct_change = self.hist_pct_change.groupby("Asset").pct_change().fillna(0)+1
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def get_positions(self, fund_name, date=dt.date.today(), recent_date=dt.date.today(), previous_date=None, period=None,
|
|
1284
|
+
get_inner_positions=False, force_month_end=False):
|
|
1285
|
+
"""
|
|
1286
|
+
Fornece um dicionario com as posicoes do fundo na data informada.
|
|
1287
|
+
"""
|
|
1288
|
+
hist_pos = self.get_table("hist_positions").query("Fund == @fund_name")
|
|
1289
|
+
|
|
1290
|
+
if (previous_date is None) and (period is None):
|
|
1291
|
+
# manter funcionamento antes da implementacao da funcionalidade de cosultar multiplas datas
|
|
1292
|
+
# Separamos posicoes historicas apenas do fundo que nos interessa antes da data informada
|
|
1293
|
+
hist_pos = hist_pos.loc[hist_pos["Date"].dt.date <= date]
|
|
1294
|
+
|
|
1295
|
+
visited = set()
|
|
1296
|
+
positions = {}
|
|
1297
|
+
rows = hist_pos.to_dict("records") # Obtendo lista de rows(dicts) para iterar
|
|
1298
|
+
|
|
1299
|
+
rows.reverse() # vamos iterar do primeiro ao ultimo
|
|
1300
|
+
|
|
1301
|
+
for row in rows:
|
|
1302
|
+
if row["Asset_ID"] not in visited:
|
|
1303
|
+
visited.add(row["Asset_ID"])
|
|
1304
|
+
if row["#"] > 0.000001 or row["#"] < -0.000001:
|
|
1305
|
+
positions[row["Asset_ID"]] = row["#"]
|
|
1306
|
+
|
|
1307
|
+
if not get_inner_positions:
|
|
1308
|
+
return positions
|
|
1309
|
+
|
|
1310
|
+
# Vamos ver se tem fundos da luxor
|
|
1311
|
+
# Vamos remover esse e explodir em posicoes internas
|
|
1312
|
+
inner_funds = {"lipizzaner_lipizzaner" : "lipizzaner",
|
|
1313
|
+
"fund a_fund a" : "fund a"}
|
|
1314
|
+
for inner_fund in inner_funds.keys():
|
|
1315
|
+
if inner_fund in positions.keys():
|
|
1316
|
+
inner_positions = self.get_positions(inner_funds[inner_fund], date,
|
|
1317
|
+
get_inner_positions=True)
|
|
1318
|
+
positions.pop(inner_fund)
|
|
1319
|
+
positions.update(inner_positions)
|
|
1320
|
+
|
|
1321
|
+
return positions
|
|
1322
|
+
|
|
1323
|
+
# Obtendo data de inicio e validando datas
|
|
1324
|
+
assert(recent_date is not None)
|
|
1325
|
+
if period is not None:
|
|
1326
|
+
previous_date = self.get_start_period_dt(recent_date, period=period, force_month_end=force_month_end)
|
|
1327
|
+
assert(recent_date > previous_date)
|
|
1328
|
+
|
|
1329
|
+
previous_or_before = hist_pos.query("Date <= @previous_date").groupby("Ticker").last().reset_index()
|
|
1330
|
+
previous_or_before["Date"] = previous_date
|
|
1331
|
+
|
|
1332
|
+
after_previous = hist_pos.query("Date > @previous_date and Date <= @recent_date")
|
|
1333
|
+
positions = pd.concat([previous_or_before, after_previous])
|
|
1334
|
+
positions["Date"] = pd.to_datetime(positions["Date"])
|
|
1335
|
+
|
|
1336
|
+
return positions
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def get_bdr_adj_price(self, asset_id, date, usdbrl_ticker="bmfxclco curncy"):
|
|
1340
|
+
|
|
1341
|
+
ticker = asset_id.split("_")[-1]
|
|
1342
|
+
price_key = self.get_price_key(asset_id)
|
|
1343
|
+
bdr_size = self.get_bdr_size(ticker)
|
|
1344
|
+
usdbrl = self.get_price(usdbrl_ticker, px_date=date)
|
|
1345
|
+
|
|
1346
|
+
return self.get_price(price_key, px_date=date) * usdbrl/bdr_size
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def normalize_trades(self, trades, currency, asset_id_columns="Asset_ID"):
|
|
1350
|
+
""" Para um dado dataframe de trades, converte para moeda indicada.
|
|
1351
|
+
Trades devem conter as colunas Asset, Ticker e Delta_Shares.
|
|
1352
|
+
BDR: Usa o peso do BDR e a quantidade de BDR operada para chegar na quantidade do ativo original.
|
|
1353
|
+
Financeiro: Corrigido pelo cambio de fechamento.
|
|
1354
|
+
|
|
1355
|
+
Args:
|
|
1356
|
+
trades (pd.DataFrame): dataframe de trades, mais especificamente o output
|
|
1357
|
+
de get_positions_and_movements.
|
|
1358
|
+
currency (str): brl; usd; all -> todas as moedas, no caso usd e brl
|
|
1359
|
+
"""
|
|
1360
|
+
assert(currency in ["usd", "brl"])
|
|
1361
|
+
previous_date=trades["Date"].min()
|
|
1362
|
+
recent_date=trades["Date"].max()
|
|
1363
|
+
|
|
1364
|
+
assets = self.get_table("assets")
|
|
1365
|
+
|
|
1366
|
+
# tratando inconsistencia da Location do spx hawker
|
|
1367
|
+
spx_hawker_brl_tickers = [
|
|
1368
|
+
"SPX SEG HAWKER JAN22", "SPX SEG HAWKER FEB22", "SPX HAWKER CL AMAR22",
|
|
1369
|
+
"SPX SEG HAWKER APR22", "SPX HAWKER CL ASET18", "SPX SEG HAWKER JUN23", "SPX SEG HAWKER SEP23"]
|
|
1370
|
+
spx_hawker_brl_tickers = list(map(str.lower, spx_hawker_brl_tickers)) # colocando tudo para minusculo
|
|
1371
|
+
assets["Location"] = np.where(assets["Ticker"].isin(spx_hawker_brl_tickers), "bz", assets["Location"])
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
# Salvando os nomes das colunas antes de editar a tabela.
|
|
1375
|
+
trades_columns = list(trades.columns)
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
# Adicionando algumas colunas que vamos precisar para conseguir distinguir os ativos
|
|
1379
|
+
trades = pd.merge(trades, assets[["Key", "Asset", "Ticker", "Type", "Location", "Ticker_BBG"]], left_on="Asset_ID", right_on="Key")
|
|
1380
|
+
|
|
1381
|
+
# Achando quantidade do trade no ativo original -> usando peso do bdr
|
|
1382
|
+
bdr_sizes = self.get_table("bdr_sizes", index=True, index_name="Ticker").reset_index()
|
|
1383
|
+
trades = pd.merge(trades, bdr_sizes, on="Ticker", how="left").rename(columns={"adr_adr_per_sh":"BDR_Size"})
|
|
1384
|
+
# Achando quantidade do trade no ativo original -> usando peso do bdr
|
|
1385
|
+
# -> Partindo da premissa que sempre atualizamos o historico quando ha alteracao do peso (inplit/split do bdr)
|
|
1386
|
+
trades["Delta_Shares"] = np.where(trades["Type"] == 'ações_bdr', trades["Delta_Shares"] / trades["BDR_Size"], trades["Delta_Shares"])
|
|
1387
|
+
# Ajustando quantidades dos trades de localiza proporcionalmente:
|
|
1388
|
+
#lcam_rent_ratio = 0.4388444 #0.44 rent3 para cada lcam3
|
|
1389
|
+
#lzrfy_rent_ratio = 1 # 1 rent para cada lzrfy
|
|
1390
|
+
#trades["Delta_Shares"] = np.where(trades["Ticker"] == 'lcam3 bz equity', trades["Delta_Shares"] * lcam_rent_ratio, trades["Delta_Shares"])
|
|
1391
|
+
# Ajuste da lzrfy eh desnecessario ja que eh 1:1
|
|
1392
|
+
#trades["Delta_Shares"] = np.where(trades["Ticker"] == 'lzrfy us equity', trades["Delta_Shares"] * lzrfy_rent_ratio, trades["Delta_Shares"])
|
|
1393
|
+
|
|
1394
|
+
#brkA_brkB_ratio = 1500
|
|
1395
|
+
#trades["Delta_Shares"] = np.where(trades["Ticker"] == 'brk/a us equity', trades["Delta_Shares"] * brkA_brkB_ratio, trades["Delta_Shares"])
|
|
1396
|
+
|
|
1397
|
+
# Adicionando coluna de usdbrl de fechamento em cada dia
|
|
1398
|
+
hist_usdbrl = self.get_prices("bmfxclco curncy", previous_date=previous_date, recent_date=recent_date).rename(columns={"Last_Price": "USDBRL"})
|
|
1399
|
+
|
|
1400
|
+
trades = pd.merge(trades, hist_usdbrl[["Date", "USDBRL"]], on="Date")
|
|
1401
|
+
|
|
1402
|
+
# Criando colunas com financeiro normalizado.
|
|
1403
|
+
if currency == 'usd':
|
|
1404
|
+
trades["Trade_Amount"] = np.where(trades["Location"] == 'bz', trades["Trade_Amount"]/trades["USDBRL"], trades["Trade_Amount"])
|
|
1405
|
+
|
|
1406
|
+
elif currency == 'brl':
|
|
1407
|
+
trades["Trade_Amount"] = np.where(trades["Location"] == 'us', trades["Trade_Amount"]*trades["USDBRL"], trades["Trade_Amount"])
|
|
1408
|
+
|
|
1409
|
+
return trades[trades_columns]
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def normalize_price_key(self, df):
|
|
1413
|
+
"""
|
|
1414
|
+
Adiciona a coluna 'Price_ID' no df informado representando uma chave normalizada para o preco do ativo desejado.
|
|
1415
|
+
Args:
|
|
1416
|
+
df (pandas.DataFrame): DataFrame que obrigatoriamente precisa conter 'Asset' e 'Ticker' como colunas.
|
|
1417
|
+
"""
|
|
1418
|
+
|
|
1419
|
+
df_columns = list(df.columns)
|
|
1420
|
+
|
|
1421
|
+
assets = self.get_table("assets")[["Key", "Asset", "Ticker", "Ticker_BBG",]].rename(columns={"Key":"Asset_ID"})
|
|
1422
|
+
|
|
1423
|
+
assets["Price_Key"] = assets["Asset_ID"].apply(lambda x: self.get_price_key(x))
|
|
1424
|
+
asset_price_key_map = {"etf s&p" : 'voo us equity', "spx hawker": "spx hawker usd", "berkshire" : "brk/b us equity",
|
|
1425
|
+
"localiza" : "rent3 bz equity", "suzano":"suzb3 bz equity"}
|
|
1426
|
+
assets["Price_Key"] = assets.apply(lambda row: asset_price_key_map[row["Asset"]] if row["Asset"] in asset_price_key_map else row["Price_Key"], axis=1)
|
|
1427
|
+
|
|
1428
|
+
df = df.rename(columns={"Price_Key": "Price_Old_key"})
|
|
1429
|
+
df = pd.merge(df, assets[["Asset_ID", "Price_Key"]], on="Asset_ID")
|
|
1430
|
+
|
|
1431
|
+
# Casos padroes |-> tratados na tabela asset, usando o get_price_key. Casos especificos adicionar no asset_price_key_map.
|
|
1432
|
+
#df["Price_Key"] = np.where((df["Ticker_BBG"] is None) or (df["Ticker_BBG"].isnull()), df["Ticker"], df["Ticker_BBG"])
|
|
1433
|
+
|
|
1434
|
+
if "Price_Key" not in df_columns: df_columns.append("Price_Key")
|
|
1435
|
+
|
|
1436
|
+
return df[df_columns]
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def calculate_average_price(self, df, asset_id="Asset_ID"):
|
|
1440
|
+
# Dictionary to store cumulative cost and total shares for each ticker
|
|
1441
|
+
ticker_info = {}
|
|
1442
|
+
|
|
1443
|
+
# Function to update ticker information
|
|
1444
|
+
def __update_ticker(ticker, delta_shares, trade_amount):
|
|
1445
|
+
if ticker not in ticker_info:
|
|
1446
|
+
ticker_info[ticker] = {'cumulative_cost': 0, 'total_shares': 0, 'avg_price': 0}
|
|
1447
|
+
|
|
1448
|
+
if delta_shares > 0: # Buy trade
|
|
1449
|
+
ticker_info[ticker]['cumulative_cost'] += -trade_amount # Trade amount is negative for buys
|
|
1450
|
+
|
|
1451
|
+
elif delta_shares < 0: # Sell trade
|
|
1452
|
+
# Update cumulative cost using the last average price
|
|
1453
|
+
ticker_info[ticker]['cumulative_cost'] -= ticker_info[ticker]['avg_price']* abs(delta_shares)
|
|
1454
|
+
|
|
1455
|
+
ticker_info[ticker]['total_shares'] += delta_shares
|
|
1456
|
+
|
|
1457
|
+
# Calculate average price
|
|
1458
|
+
if abs(ticker_info[ticker]['total_shares']) > 0.001:
|
|
1459
|
+
ticker_info[ticker]["avg_price"] = ticker_info[ticker]['cumulative_cost'] / ticker_info[ticker]['total_shares']
|
|
1460
|
+
|
|
1461
|
+
else:
|
|
1462
|
+
ticker_info[ticker]["avg_price"] = np.nan # Avoid division by zero
|
|
1463
|
+
|
|
1464
|
+
return ticker_info[ticker]["avg_price"]
|
|
1465
|
+
|
|
1466
|
+
# Apply the function to each row and create a new column for the average price
|
|
1467
|
+
df['Average_Price'] = df.apply(lambda row: __update_ticker(row[asset_id], row['Delta_Shares'], row['Trade_Amount']), axis=1)
|
|
1468
|
+
|
|
1469
|
+
return df
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def calculate_adjusted_avg_price(self, df, groupby_column="Ticker"):
|
|
1474
|
+
"""Recebe um DataFrame contendo as colunas Date, Ticker, Delta_Shares e Trade_Amount.
|
|
1475
|
+
Trade_Amount sera resultado do preco_medio anterior (ja calculado na transacao)
|
|
1476
|
+
multiplicado pela quantidade operada. ATENCAO pois na venda nao tem a informacao
|
|
1477
|
+
do valor pelo qual o ativo foi vendido.
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
df (pandas.DataFrame):
|
|
1481
|
+
|
|
1482
|
+
Returns:
|
|
1483
|
+
pandas.DataFrame: Adiciona as colunas Cumulative_Shares, Open_Position_Cost e Average_Price
|
|
1484
|
+
"""
|
|
1485
|
+
# Sort the DataFrame by Ticker and Date for sequential processing
|
|
1486
|
+
df_sorted = df.sort_values([groupby_column, "Date", "Trade_Side"]) # sort by trade_side (para processar sempre a compra primeiro)
|
|
1487
|
+
df_columns = list(df.columns)
|
|
1488
|
+
|
|
1489
|
+
# Calculate cumulative shares and trade value for each ticker
|
|
1490
|
+
df_sorted['Cumulative_Shares'] = df_sorted.groupby(groupby_column)['Delta_Shares'].cumsum()
|
|
1491
|
+
|
|
1492
|
+
df_sorted = self.calculate_average_price(df_sorted)
|
|
1493
|
+
df_sorted["Average_Price"] = df_sorted.groupby(["Date", "Ticker", "Trade_Side"])["Average_Price"].ffill()#fillna(method="ffill")
|
|
1494
|
+
|
|
1495
|
+
df_sorted["Open_Position_Cost"] = -abs(df_sorted["Cumulative_Shares"] * df_sorted["Average_Price"])
|
|
1496
|
+
|
|
1497
|
+
return df_sorted[df_columns+["Cumulative_Shares", "Open_Position_Cost", "Average_Price"]]
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def __get_positions_and_movements(self, fund_name, tickers=None,
|
|
1501
|
+
recent_date=dt.date.today(), previous_date=dt.date.today()-dt.timedelta(days=1),
|
|
1502
|
+
period=None, holiday_location="all", currency="usd"):
|
|
1503
|
+
"""
|
|
1504
|
+
Args:
|
|
1505
|
+
fund_name (str):
|
|
1506
|
+
tickers (str|list|set, optional): Defaults to None.
|
|
1507
|
+
recent_date (datetime.date, optional): Defaults to dt.date.today().
|
|
1508
|
+
previous_date (datetime.date, optional): _description_. Defaults to dt.date.today()-dt.timedelta(days=1).
|
|
1509
|
+
period (str, optional): mtd|ytd|3m|6m|...|nm. Defaults to None.
|
|
1510
|
+
holiday_location (str, optional): any|bz|us|all. Defaults to "all".
|
|
1511
|
+
|
|
1512
|
+
Returns:
|
|
1513
|
+
pandas.DataFrame:
|
|
1514
|
+
"""
|
|
1515
|
+
|
|
1516
|
+
assert(currency in ["usd", "brl"])
|
|
1517
|
+
|
|
1518
|
+
if period is not None:
|
|
1519
|
+
previous_date = self.get_start_period_dt(recent_date, period, holiday_location=holiday_location, force_bday=True)
|
|
1520
|
+
|
|
1521
|
+
# Garantindo que a data inicial sera sempre dia util, assim eh certo de que havera preco de acao nessa data
|
|
1522
|
+
previous_date = self.get_bday_offset(previous_date, offset=0, location="any")
|
|
1523
|
+
lipi_manga_incorp_date = self.lipi_manga_incorp_date
|
|
1524
|
+
assets = self.get_table("assets")
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
# Filtrando somente os trades desejados
|
|
1528
|
+
# lembrando que lipi precisa abrir em fund_A e manga_master (quando antes da incorporacao)
|
|
1529
|
+
# ha ainda um ajuste no lipi, que na data da incorporacao compra as posicoes do Manga (nao podemos tirar essas boletas)
|
|
1530
|
+
trades = (self.get_table("trades")[["Date", "Fund", "Asset", "Ticker", "#", "Price", "Total", "Op Type"]]
|
|
1531
|
+
.rename(columns={"#":"Delta_Shares", "Total" : "Trade_Amount", "Op Type":"Op_Type"})
|
|
1532
|
+
.query("(Date > @previous_date and Date <= @recent_date) and Op_Type.isin(['a vista', 'ndf', 'termo', 'apl/resg fundos', 'vc capital call', 'vc'])"))
|
|
1533
|
+
|
|
1534
|
+
fund_filter = [fund_name]
|
|
1535
|
+
if fund_name == 'lipizzaner':
|
|
1536
|
+
fund_filter += ["fund a"]
|
|
1537
|
+
if previous_date < lipi_manga_incorp_date:
|
|
1538
|
+
fund_filter += ["mangalarga master"]
|
|
1539
|
+
trades = trades.query("Fund in @fund_filter")
|
|
1540
|
+
|
|
1541
|
+
trades = trades.query("Fund != 'lipizzaner' or Date != @lipi_manga_incorp_date").rename(columns={"Price" : "Exec_Price"}).copy()
|
|
1542
|
+
trades["Asset_ID"] = trades["Asset"] + "_" + trades["Ticker"]
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
# Obtendo posicoes na data de inicio, e criando boletas de inicializacao
|
|
1546
|
+
initial_positions = []
|
|
1547
|
+
|
|
1548
|
+
for f in fund_filter:
|
|
1549
|
+
fund_initial_pos = pd.DataFrame(self.get_positions(f, date=previous_date).items(), columns=["Asset_ID", "Delta_Shares"])
|
|
1550
|
+
if len(fund_initial_pos) == 0: continue
|
|
1551
|
+
fund_initial_pos["Fund"] = f
|
|
1552
|
+
fund_initial_pos[["Asset", "Ticker"]] = tuple(fund_initial_pos["Asset_ID"].str.split("_"))
|
|
1553
|
+
|
|
1554
|
+
initial_positions.append(fund_initial_pos)
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
if len(initial_positions) > 0:
|
|
1558
|
+
initial_positions = pd.concat(initial_positions)
|
|
1559
|
+
|
|
1560
|
+
# Adicionando coluna de tipo, pois sera necessario tratar quando type == ações_bdr
|
|
1561
|
+
initial_positions = pd.merge(initial_positions, assets[["Key", "Type"]], left_on="Asset_ID", right_on="Key")
|
|
1562
|
+
|
|
1563
|
+
initial_positions["Price_Key"] = initial_positions["Asset_ID"].apply(lambda x: self.get_price_key(x))
|
|
1564
|
+
initial_prices = (self.get_prices(tickers=list(initial_positions["Price_Key"]), previous_date=previous_date,
|
|
1565
|
+
recent_date=previous_date, force_continuous_date_range=True)
|
|
1566
|
+
.rename(columns={"Asset": "Price_Key", "Last_Price":"Exec_Price"})[["Price_Key", "Exec_Price"]])
|
|
1567
|
+
|
|
1568
|
+
initial_positions = pd.merge(initial_positions, initial_prices, on=["Price_Key"])
|
|
1569
|
+
|
|
1570
|
+
# Corrigindo preço de entrada das BDRs
|
|
1571
|
+
initial_positions["Exec_Price"] = initial_positions.apply(lambda row: row["Exec_Price"] if row["Type"] != 'ações_bdr' else self.get_bdr_adj_price(row["Asset_ID"], date=previous_date), axis=1)
|
|
1572
|
+
|
|
1573
|
+
initial_positions["Trade_Amount"] = initial_positions["Delta_Shares"] * initial_positions["Exec_Price"] * (-1)
|
|
1574
|
+
initial_positions["Date"] = previous_date
|
|
1575
|
+
|
|
1576
|
+
else:
|
|
1577
|
+
initial_positions = pd.DataFrame() # tratando caso inicial, onde nao ha posicoes no inicio
|
|
1578
|
+
|
|
1579
|
+
trades = pd.concat([initial_positions, trades])[["Date", "Asset_ID", "Delta_Shares", "Exec_Price", "Trade_Amount"]]
|
|
1580
|
+
|
|
1581
|
+
trades["Date"] = pd.to_datetime(trades["Date"])
|
|
1582
|
+
|
|
1583
|
+
# Adicionando coluna pra marcar se eh compra ou venda. Sera usado na agregacao logo mais.
|
|
1584
|
+
#trades["Trade_Side"] = np.where(trades["Delta_Shares"] < 0, "sell", "buy")
|
|
1585
|
+
|
|
1586
|
+
# Ordenando boletas, por padrao vamos sempre comprar antes de vender.
|
|
1587
|
+
trades = self.normalize_trades(trades, currency=currency).sort_values(by=["Date"], kind="stable")
|
|
1588
|
+
# Apos normalizar os trades, eh necessario recalcular o preco da boleta
|
|
1589
|
+
|
|
1590
|
+
# Agrupando boletas de forma a ter apenas uma boleta por dia por codigo de ativo
|
|
1591
|
+
#trades = trades.groupby(["Date", "Asset_ID"]).agg({"Delta_Shares":"sum", "Exec_Price":"last", "Trade_Amount":"sum"}).reset_index()
|
|
1592
|
+
# Precisamos calcular novamente o preco correto de execucao do trade
|
|
1593
|
+
#trades["Exec_Price"] = abs(trades["Trade_Amount"])/abs(trades["Delta_Shares"])
|
|
1594
|
+
|
|
1595
|
+
# Calculando posicao ao final de cada trade, preco medio e custo da posicao
|
|
1596
|
+
trades["Open_Quantity"] = trades.groupby(["Asset_ID"])["Delta_Shares"].cumsum()
|
|
1597
|
+
trades["Open_Quantity"] = np.where(abs(trades["Open_Quantity"]) < 0.00001, 0, trades["Open_Quantity"])
|
|
1598
|
+
trades = self.calculate_average_price(trades)
|
|
1599
|
+
trades["Average_Price"] = trades.groupby(["Asset_ID"])["Average_Price"].ffill()#fillna(method="ffill")
|
|
1600
|
+
|
|
1601
|
+
# Vamos agregar novamente, agora para tirar o 'Trade_Side', passando a ter uma linha apenas por data
|
|
1602
|
+
#trades = trades.groupby(["Date", "Asset_ID"]).agg({"Delta_Shares":"sum", "Exec_Price":"last", "Trade_Amount":"sum",
|
|
1603
|
+
# "Open_Quantity":"last", "Average_Price" : "last", "Trade_Side":"last"}).reset_index()
|
|
1604
|
+
# Preco de execucao, necessita ser recalculado
|
|
1605
|
+
#trades["Exec_Price"] = abs(trades["Trade_Amount"])/abs(trades["Delta_Shares"])
|
|
1606
|
+
|
|
1607
|
+
trades["Open_Position_Cost"] = (trades["Open_Quantity"] * trades["Average_Price"])
|
|
1608
|
+
|
|
1609
|
+
return trades
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def get_positions_and_movements(self, fund_name, tickers=None,
|
|
1613
|
+
recent_date=dt.date.today(), previous_date=dt.date.today()-dt.timedelta(days=1),
|
|
1614
|
+
period=None, holiday_location="all", currency="usd"):
|
|
1615
|
+
|
|
1616
|
+
"""
|
|
1617
|
+
Args:
|
|
1618
|
+
fund_name (str):
|
|
1619
|
+
tickers (str|list|set, optional): Defaults to None.
|
|
1620
|
+
recent_date (datetime.date, optional): Defaults to dt.date.today().
|
|
1621
|
+
previous_date (datetime.date, optional): _description_. Defaults to dt.date.today()-dt.timedelta(days=1).
|
|
1622
|
+
period (str, optional): mtd|ytd|3m|6m|...|nm. Defaults to None.
|
|
1623
|
+
holiday_location (str, optional): any|bz|us|all. Defaults to "all".
|
|
1624
|
+
|
|
1625
|
+
Returns:
|
|
1626
|
+
pandas.DataFrame:
|
|
1627
|
+
"""
|
|
1628
|
+
|
|
1629
|
+
df = self.__run_return_analysis(fund_name, tickers=tickers,
|
|
1630
|
+
recent_date=recent_date, previous_date=previous_date,
|
|
1631
|
+
period=period, holiday_location=holiday_location,
|
|
1632
|
+
currency=currency, agregate_by_name=False
|
|
1633
|
+
)
|
|
1634
|
+
# Vamos obter os precos de fechamento em cada dia
|
|
1635
|
+
# Preenchendo primeiro todo o historico
|
|
1636
|
+
prices_previous_date = (df["Date"].min() - dt.timedelta(days=100)).date()
|
|
1637
|
+
price_keys = list(df["Price_Key"].unique())
|
|
1638
|
+
prices = self.get_prices(
|
|
1639
|
+
tickers=price_keys, previous_date=prices_previous_date, currency=currency,
|
|
1640
|
+
get_intraday_prices=True
|
|
1641
|
+
).rename(columns={"Asset":"Price_Key"})
|
|
1642
|
+
|
|
1643
|
+
df = df.merge(prices, on=["Date", "Price_Key"], how="left")
|
|
1644
|
+
# Finalmente, vamos atualizar com o preco mais recente do ativo na data de hoje
|
|
1645
|
+
#df_prev = df.query("Date < @dt.date.today()").copy()
|
|
1646
|
+
#df_today = df.query("Date == @dt.date.today()").copy()
|
|
1647
|
+
#assets = self.get_table("assets").rename(columns={"Key":"Asset_ID"})
|
|
1648
|
+
#df_today = df_today.merge(assets[["Asset_ID", "Location"]], on="Asset_ID")
|
|
1649
|
+
#if len(df_today) > 0:
|
|
1650
|
+
# df_today["Last_Price"] = df_today.apply(lambda row: self.get_price(
|
|
1651
|
+
# row["Price_Key"], currency=currency,
|
|
1652
|
+
# usdbrl_ticker="usdbrl curncy",
|
|
1653
|
+
# asset_location=row["Location"]), axis=1
|
|
1654
|
+
# )
|
|
1655
|
+
# df = pd.concat([df_prev, df_today])
|
|
1656
|
+
#else:
|
|
1657
|
+
# df = df_prev
|
|
1658
|
+
|
|
1659
|
+
df["Current_Position_Value"] = df["Open_Quantity"] * df["Last_Price"]
|
|
1660
|
+
|
|
1661
|
+
df = df[["Date", "Asset_ID", "Price_Key", "Open_Quantity",
|
|
1662
|
+
"Delta_Shares", "Shares_Bought", "Shares_Bought_Cost", "Shares_Sold",
|
|
1663
|
+
"Amount_Sold", "Last_Price", "Current_Position_Value"]]
|
|
1664
|
+
|
|
1665
|
+
# preenchendo Last_Price nulos com o do dia anterior para os mesmos asset_id
|
|
1666
|
+
df["Last_Price"] = df.groupby("Asset_ID")["Last_Price"].ffill()
|
|
1667
|
+
|
|
1668
|
+
return df
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
def run_return_analysis(self, fund_name, tickers=None, recent_date=dt.date.today(), previous_date=dt.date.today()-dt.timedelta(days=1), period=None, holiday_location="all"):
|
|
1672
|
+
|
|
1673
|
+
currency = "usd"
|
|
1674
|
+
positions_and_movements_usd = self.__run_return_analysis(fund_name, tickers=tickers, recent_date=recent_date,
|
|
1675
|
+
previous_date=previous_date, period=period,
|
|
1676
|
+
holiday_location=holiday_location, currency=currency)
|
|
1677
|
+
currency = "brl"
|
|
1678
|
+
|
|
1679
|
+
positions_and_movements_brl = self.__run_return_analysis(fund_name, tickers=tickers, recent_date=recent_date,
|
|
1680
|
+
previous_date=previous_date, period=period,
|
|
1681
|
+
holiday_location=holiday_location, currency=currency)
|
|
1682
|
+
if (positions_and_movements_usd is None) and (positions_and_movements_brl is None): return None
|
|
1683
|
+
|
|
1684
|
+
return pd.concat([positions_and_movements_usd, positions_and_movements_brl])
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def __expand_hist_positions(self, hist_positions, recent_date, previous_date, holiday_location="all"):
|
|
1688
|
+
|
|
1689
|
+
#for asset_id in positions_and_movements["Asset_ID"].unique():
|
|
1690
|
+
# first_trade_date = positions_and_movements[positions_and_movements['Asset_ID'] == asset_id].index.min()
|
|
1691
|
+
# date_range = pd.date_range(start=first_trade_date, end=recent_date, freq='D')
|
|
1692
|
+
# ticker_transactions = positions_and_movements[positions_and_movements['Asset_ID'] == asset_id]
|
|
1693
|
+
|
|
1694
|
+
# temp_df = pd.DataFrame({'Date': date_range, 'Asset_ID': asset_id})
|
|
1695
|
+
# temp_df = temp_df.merge(ticker_transactions, on=['Date', 'Asset_ID'], how='left')
|
|
1696
|
+
# columns_to_ffill = ["Asset_ID", 'Open_Quantity',"Open_Position_Cost", "Average_Price"]
|
|
1697
|
+
# temp_df[columns_to_ffill] = temp_df[columns_to_ffill].ffill()
|
|
1698
|
+
# columns_to_fill_zeros = ["Delta_Shares","Trade_Amount", "Exec_Price", "Shares_Bought",
|
|
1699
|
+
# "Shares_Sold", "Shares_Bought_Cost", "Amount_Sold", "Cost_Of_Stocks_Sold", "Result_Of_Stocks_Sold"]
|
|
1700
|
+
#
|
|
1701
|
+
# temp_df[columns_to_fill_zeros] = temp_df[columns_to_fill_zeros].fillna(0)
|
|
1702
|
+
#
|
|
1703
|
+
# daily_positions = pd.concat([daily_positions, temp_df])
|
|
1704
|
+
pass
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def __run_return_analysis(self, fund_name, tickers=None, recent_date=dt.date.today(),
|
|
1708
|
+
previous_date=dt.date.today()-dt.timedelta(days=1), period=None, holiday_location="all",
|
|
1709
|
+
currency="usd", agregate_by_name=True):
|
|
1710
|
+
|
|
1711
|
+
assert(currency in ["usd", "brl"])
|
|
1712
|
+
|
|
1713
|
+
positions_and_movements = self.__get_positions_and_movements(fund_name, tickers, recent_date, previous_date, period, holiday_location, currency=currency)
|
|
1714
|
+
|
|
1715
|
+
# -> Shares_Bought : Delta_Shares > 0
|
|
1716
|
+
positions_and_movements["Shares_Bought"] = np.where(positions_and_movements["Delta_Shares"] > 0, positions_and_movements["Delta_Shares"], 0)
|
|
1717
|
+
# -> Shares_Sold : Delta_Shares < 0
|
|
1718
|
+
positions_and_movements["Shares_Sold"] = np.where(positions_and_movements["Delta_Shares"] < 0, positions_and_movements["Delta_Shares"], 0)
|
|
1719
|
+
# -> Shares_Bought_Cost: eh o trade amount
|
|
1720
|
+
positions_and_movements["Shares_Bought_Cost"] = np.where(positions_and_movements["Delta_Shares"] > 0, abs(positions_and_movements["Trade_Amount"]), 0)
|
|
1721
|
+
# -> Amount_Sold : Shares_Sold * Exec_Price
|
|
1722
|
+
positions_and_movements["Amount_Sold"] = np.where(positions_and_movements["Delta_Shares"] < 0, abs(positions_and_movements["Trade_Amount"]), 0)
|
|
1723
|
+
# -> Cost_Of_Stocks_Sold : Shares_Sold * avg_price # vai dar ruim na zeragem, posso propagar o avg_price ao inves do cumulative_cost? Feito.
|
|
1724
|
+
positions_and_movements["Cost_Of_Stocks_Sold"] =abs( positions_and_movements["Shares_Sold"] * positions_and_movements["Average_Price"])
|
|
1725
|
+
# -> Result_Of_Stocks_Sold: Total_Sold - Cost_Of_Stocks_Sold
|
|
1726
|
+
positions_and_movements["Result_Of_Stocks_Sold"] = abs(positions_and_movements["Amount_Sold"]) - positions_and_movements["Cost_Of_Stocks_Sold"]
|
|
1727
|
+
|
|
1728
|
+
#positions_and_movements.to_excel(f"testando_{currency}.xlsx")
|
|
1729
|
+
# Transformar granularidade pra diario somente aqui...
|
|
1730
|
+
agg_rule = {
|
|
1731
|
+
'Open_Quantity':"last", "Average_Price":"last",
|
|
1732
|
+
"Delta_Shares":"sum","Trade_Amount":"sum", "Exec_Price":"last", "Open_Position_Cost":"last", "Shares_Bought":"sum",
|
|
1733
|
+
"Shares_Sold":"sum", "Shares_Bought_Cost":"sum", "Amount_Sold":"sum", "Cost_Of_Stocks_Sold":"sum", "Result_Of_Stocks_Sold":"sum"
|
|
1734
|
+
}
|
|
1735
|
+
positions_and_movements = positions_and_movements.groupby(["Date", "Asset_ID"]).agg(agg_rule).reset_index()
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
# diminuindo a granularidade de 1 linha por trade pra 1 linha por dia
|
|
1739
|
+
positions_and_movements = positions_and_movements.set_index("Date")
|
|
1740
|
+
|
|
1741
|
+
daily_positions = pd.DataFrame()
|
|
1742
|
+
|
|
1743
|
+
for asset_id in positions_and_movements["Asset_ID"].unique():
|
|
1744
|
+
first_trade_date = positions_and_movements[positions_and_movements['Asset_ID'] == asset_id].index.min()
|
|
1745
|
+
date_range = pd.date_range(start=first_trade_date, end=recent_date, freq='D')
|
|
1746
|
+
ticker_transactions = positions_and_movements[positions_and_movements['Asset_ID'] == asset_id]
|
|
1747
|
+
|
|
1748
|
+
temp_df = pd.DataFrame({'Date': date_range, 'Asset_ID': asset_id})
|
|
1749
|
+
temp_df = temp_df.merge(ticker_transactions, on=['Date', 'Asset_ID'], how='left')
|
|
1750
|
+
columns_to_ffill = ["Asset_ID", 'Open_Quantity',"Open_Position_Cost", "Average_Price"]
|
|
1751
|
+
|
|
1752
|
+
temp_df[columns_to_ffill] = temp_df[columns_to_ffill].infer_objects(copy=False).ffill()
|
|
1753
|
+
columns_to_fill_zeros = ["Delta_Shares","Trade_Amount", "Exec_Price", "Shares_Bought",
|
|
1754
|
+
"Shares_Sold", "Shares_Bought_Cost", "Amount_Sold", "Cost_Of_Stocks_Sold", "Result_Of_Stocks_Sold"]
|
|
1755
|
+
|
|
1756
|
+
temp_df[columns_to_fill_zeros] = temp_df[columns_to_fill_zeros].astype(float).fillna(0)
|
|
1757
|
+
|
|
1758
|
+
daily_positions = pd.concat([daily_positions, temp_df])
|
|
1759
|
+
|
|
1760
|
+
positions_and_movements = daily_positions.copy()
|
|
1761
|
+
|
|
1762
|
+
# -> Criar coluna price_key
|
|
1763
|
+
assets = self.get_table("assets")
|
|
1764
|
+
assets["Price_Key"] = assets["Key"].apply(lambda x: self.get_price_key(x))
|
|
1765
|
+
|
|
1766
|
+
# -> Fazer merge com Asset_ID, colocando colunas Name, Class e Price_Key
|
|
1767
|
+
positions_and_movements = positions_and_movements.merge(assets[["Key", "Name", "Class", "Price_Key"]], left_on="Asset_ID", right_on="Key")
|
|
1768
|
+
|
|
1769
|
+
if not agregate_by_name:
|
|
1770
|
+
return positions_and_movements
|
|
1771
|
+
|
|
1772
|
+
# -> pegar precos e colocar no diario
|
|
1773
|
+
previous_date = positions_and_movements["Date"].min().date()
|
|
1774
|
+
prices = self.get_prices(tickers = list(positions_and_movements["Price_Key"].unique()), previous_date=previous_date-dt.timedelta(days=100), currency=currency).rename(columns={"Asset":"Price_Key"})
|
|
1775
|
+
|
|
1776
|
+
positions_and_movements = positions_and_movements.merge(prices, on=["Date", "Price_Key"])
|
|
1777
|
+
|
|
1778
|
+
# -> Criar coluna Market_Value
|
|
1779
|
+
positions_and_movements["Current_Position_Value"] = positions_and_movements["Open_Quantity"] * positions_and_movements["Last_Price"]
|
|
1780
|
+
|
|
1781
|
+
# -> Open_Result_Amount: Current_Position_Value - avg_price * Open_Quantity Se nao fizer assim vai dar problema no dia da zeragem
|
|
1782
|
+
positions_and_movements["Open_Result_Amount"] = positions_and_movements["Current_Position_Value"] - positions_and_movements["Open_Position_Cost"]
|
|
1783
|
+
|
|
1784
|
+
# -> valido salvar uma versao nessa etapa para conferir
|
|
1785
|
+
#positions_and_movements.to_excel("testing.xlsx")
|
|
1786
|
+
# -> Apos essa agregacao, nao se pode mais usar o preco medio <-
|
|
1787
|
+
# -> Agrupar por Name
|
|
1788
|
+
positions_and_movements = positions_and_movements[[
|
|
1789
|
+
'Date', 'Asset_ID', 'Trade_Amount', # 'Delta_Shares', 'Exec_Price'
|
|
1790
|
+
'Open_Quantity', 'Open_Position_Cost', 'Name', #'Average_Price', 'Key'
|
|
1791
|
+
'Class', 'Price_Key', 'Current_Position_Value', # 'Last_Price'
|
|
1792
|
+
'Shares_Bought', 'Shares_Sold', 'Shares_Bought_Cost',
|
|
1793
|
+
'Amount_Sold', 'Cost_Of_Stocks_Sold', 'Result_Of_Stocks_Sold',
|
|
1794
|
+
'Open_Result_Amount']]
|
|
1795
|
+
|
|
1796
|
+
agg_rule = { 'Asset_ID' : "last", 'Trade_Amount' : "sum", 'Open_Quantity' : "sum", 'Open_Position_Cost' : "sum",
|
|
1797
|
+
'Class' : "last", 'Price_Key' : "last", 'Current_Position_Value' : 'sum',
|
|
1798
|
+
'Shares_Bought' : "sum", 'Shares_Sold' : "sum", 'Shares_Bought_Cost' : "sum",
|
|
1799
|
+
'Amount_Sold' : "sum", 'Cost_Of_Stocks_Sold' : "sum", 'Result_Of_Stocks_Sold' : "sum",
|
|
1800
|
+
'Open_Result_Amount' : "sum"}
|
|
1801
|
+
|
|
1802
|
+
positions_and_movements = positions_and_movements.groupby(["Date", "Name"]).agg(agg_rule).reset_index()
|
|
1803
|
+
|
|
1804
|
+
# -> Acc_Result_Of_Stocks_Sold : Acc_Result_Of_Stocks_Sold.cumsum()
|
|
1805
|
+
positions_and_movements["Acc_Result_Of_Stocks_Sold"] = positions_and_movements.groupby("Name")["Result_Of_Stocks_Sold"].cumsum()
|
|
1806
|
+
# -> Tota_Result_Amount: Acc_Result_Of_Stocks_Sold + Open_Result_Amount
|
|
1807
|
+
positions_and_movements["Total_Result_Amount"] = positions_and_movements["Acc_Result_Of_Stocks_Sold"] + positions_and_movements["Open_Result_Amount"]
|
|
1808
|
+
# -> Acc_Cost_Of_Stocks_Sold : Cost_Of_Stocks_Sold.cumsum()
|
|
1809
|
+
positions_and_movements["Acc_Cost_Of_Stocks_Sold"] = positions_and_movements.groupby("Name")["Cost_Of_Stocks_Sold"].cumsum()
|
|
1810
|
+
# -> Acc_Buy_Cost: Total_Bough.cumsum()
|
|
1811
|
+
positions_and_movements["Acc_Shares_Bought_Cost"] = positions_and_movements.groupby("Name")["Shares_Bought_Cost"].cumsum()
|
|
1812
|
+
|
|
1813
|
+
# -> %_Closed_Result: Acc_Result_Of_Stocks_Sold/Acc_Cost_Of_Stocks_Sold
|
|
1814
|
+
positions_and_movements["%_Closed_Result"] = positions_and_movements["Acc_Result_Of_Stocks_Sold"]/abs(positions_and_movements["Acc_Cost_Of_Stocks_Sold"])
|
|
1815
|
+
# -> %_Open_Result: Current_Position_Value/Open_Position_Cost
|
|
1816
|
+
positions_and_movements["%_Open_Result"] = (positions_and_movements["Current_Position_Value"]/positions_and_movements["Open_Position_Cost"])-1
|
|
1817
|
+
# -> %_Total_Result: Total_Result_Amount/Acc_Buy_Cost
|
|
1818
|
+
|
|
1819
|
+
positions_and_movements["Avg_Cost"] = (positions_and_movements[[
|
|
1820
|
+
"Name","Open_Position_Cost"]].groupby("Name")
|
|
1821
|
+
["Open_Position_Cost"].cumsum())
|
|
1822
|
+
positions_and_movements["Avg_Cost"] = (positions_and_movements["Avg_Cost"])/(positions_and_movements
|
|
1823
|
+
.groupby("Name").cumcount() + 1)
|
|
1824
|
+
|
|
1825
|
+
#positions_and_movements["%_Total_Result"] = positions_and_movements["Total_Result_Amount"]/positions_and_movements["Acc_Shares_Bought_Cost"]
|
|
1826
|
+
positions_and_movements["%_Total_Result"] = positions_and_movements["Total_Result_Amount"]/positions_and_movements["Avg_Cost"]
|
|
1827
|
+
# -> redefinir price_key e price -> Precisa tratar nessa funcao o caso a caso do ticker de preco que sera mostrado
|
|
1828
|
+
positions_and_movements = self.normalize_price_key(positions_and_movements)
|
|
1829
|
+
# A normalizacao pode acabar inserindo algum ticker generico que ainda nao estava calculado. Logo, temos que pegar os precos novamente.
|
|
1830
|
+
prices = self.get_prices(tickers = list(positions_and_movements["Price_Key"].unique()),
|
|
1831
|
+
previous_date=previous_date-dt.timedelta(days=100), currency=currency).rename(columns={"Asset":"Price_Key"})
|
|
1832
|
+
|
|
1833
|
+
positions_and_movements = positions_and_movements.merge(prices[["Date", "Price_Key", "Last_Price"]], on=["Date","Price_Key"])
|
|
1834
|
+
|
|
1835
|
+
positions_and_movements["Period"] = period
|
|
1836
|
+
positions_and_movements["Currency"] = currency
|
|
1837
|
+
# Consideramos como ativos atuais aqueles que possuem posicao aberta ou que foram encerradas no dia
|
|
1838
|
+
positions_and_movements["Is_Curr_Owned"] = abs(positions_and_movements["Open_Position_Cost"]) > 0.1
|
|
1839
|
+
positions_and_movements["Is_Curr_Owned_Prev"] = positions_and_movements.groupby("Name")["Is_Curr_Owned"].shift(1,fill_value=False)
|
|
1840
|
+
positions_and_movements["Is_Curr_Owned"] = positions_and_movements["Is_Curr_Owned"] | positions_and_movements["Is_Curr_Owned_Prev"]
|
|
1841
|
+
positions_and_movements = positions_and_movements.drop(columns=["Is_Curr_Owned_Prev"])
|
|
1842
|
+
|
|
1843
|
+
positions_and_movements["Start_Date"] = positions_and_movements["Name"].map(positions_and_movements.groupby("Name")["Date"].min())
|
|
1844
|
+
positions_and_movements["End_Date"] = recent_date
|
|
1845
|
+
positions_and_movements["End_Date"] = pd.to_datetime(positions_and_movements["End_Date"])
|
|
1846
|
+
last_trades = positions_and_movements.query("Trade_Amount != 0").groupby("Name").tail(1).copy().set_index("Asset_ID")
|
|
1847
|
+
last_trades["End_Date"] = np.where(abs(last_trades["Open_Quantity"]) <0.00001, last_trades["Date"], last_trades["End_Date"])
|
|
1848
|
+
positions_and_movements["End_Date"] = positions_and_movements["Asset_ID"].map(last_trades["End_Date"])
|
|
1849
|
+
|
|
1850
|
+
# Removendo erros de divisao por 0
|
|
1851
|
+
positions_and_movements["%_Closed_Result"] = np.where(abs(positions_and_movements["%_Closed_Result"]) == np.inf, 0, positions_and_movements["%_Closed_Result"])
|
|
1852
|
+
positions_and_movements["%_Open_Result"] = np.where(abs(positions_and_movements["%_Open_Result"]) == np.inf, 0, positions_and_movements["%_Open_Result"])
|
|
1853
|
+
positions_and_movements["%_Total_Result"] = np.where(abs(positions_and_movements["%_Total_Result"]) == np.inf, 0, positions_and_movements["%_Total_Result"])
|
|
1854
|
+
# Refinando, para garantir dados consistentes
|
|
1855
|
+
positions_and_movements["%_Open_Result"] = np.where(abs(positions_and_movements["Open_Quantity"]) < 0.00001, 0, positions_and_movements["%_Open_Result"])
|
|
1856
|
+
|
|
1857
|
+
positions_and_movements["Security_Acc_Return"] = positions_and_movements[["Name","Last_Price"]].groupby(["Name"]).pct_change().fillna(0) + 1
|
|
1858
|
+
positions_and_movements["Security_Acc_Return"] = positions_and_movements[["Name","Security_Acc_Return"]].groupby(["Name"]).cumprod()-1
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
positions_and_movements["Days_Since_Start"] = (positions_and_movements["Date"] - positions_and_movements["Start_Date"]).dt.days
|
|
1862
|
+
|
|
1863
|
+
spx_prices = self.get_prices("inx index", recent_date=recent_date, previous_date=previous_date, currency=currency)[["Date", "Last_Price"]].rename(columns={"Last_Price" : "spx_index"})
|
|
1864
|
+
positions_and_movements = positions_and_movements.merge(spx_prices, on="Date", how="left")
|
|
1865
|
+
|
|
1866
|
+
#positions_and_movements["spx_index"] = (positions_and_movements["spx_index"].pct_change().fillna(0)+1).cumprod()-1
|
|
1867
|
+
|
|
1868
|
+
return positions_and_movements.rename(columns={"Price_Key":"Ticker"})
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def get_current_fx_data(self, fund, fx_ticker):
|
|
1872
|
+
"""Calcula o PNL de FX de um fundo. -> CONSIDERANDO QUE EH USDBRL"""
|
|
1873
|
+
fx_positions = self.get_table("last_positions").query("Fund == @fund and Ticker == @fx_ticker").copy()
|
|
1874
|
+
fx_positions["Last_Price"] = fx_positions["Ticker"].apply(lambda x: self.get_price(x))
|
|
1875
|
+
fx_positions["Pnl_USD"] = fx_positions["#"] * ((fx_positions["Last_Price"]/fx_positions["Avg_price"]) - 1)
|
|
1876
|
+
fx_positions["Pnl_BRL"] = fx_positions["#"] * (fx_positions["Last_Price"] - fx_positions["Avg_price"])
|
|
1877
|
+
fx_positions["Exposure"] = fx_positions["#"] * fx_positions["Last_Price"]
|
|
1878
|
+
fx_positions = fx_positions[["Pnl_USD", "Pnl_BRL", "Exposure"]].squeeze().to_dict()
|
|
1879
|
+
|
|
1880
|
+
return fx_positions
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
def get_hist_cash_movements(self, fund, ref_date, bdays=10,
|
|
1884
|
+
holiday_location="all", currency="usd", usdbrl_ticker = "bmfxclco curncy"):
|
|
1885
|
+
"""Retorna o historico de caixa do fundo.
|
|
1886
|
+
Args:
|
|
1887
|
+
fund (str): nome do fundo
|
|
1888
|
+
currency (str): moeda (usd|brl)
|
|
1889
|
+
|
|
1890
|
+
Returns:
|
|
1891
|
+
pandas.DataFrame: historico de caixa
|
|
1892
|
+
"""
|
|
1893
|
+
|
|
1894
|
+
## TODO: Remover linhas com pnl e retorno vazios!
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
fund = fund.replace(' ', '_').lower()
|
|
1898
|
+
previous_date = self.get_bday_offset(ref_date, -bdays,
|
|
1899
|
+
location=holiday_location)
|
|
1900
|
+
|
|
1901
|
+
cash = self.get_table("hist_portfolios_concentration").query("Group.isin(['caixa','caixa_us']) and Fund_Name == @fund").copy()
|
|
1902
|
+
cash["Date"] = cash["Date"].dt.date
|
|
1903
|
+
cash = cash.query("Date >= @previous_date").copy()
|
|
1904
|
+
# cash estara com ambos os caixas em R$ ou U$.
|
|
1905
|
+
# Para saber a moeda do caixa retornado, precisamos saber a moeda do fundo
|
|
1906
|
+
fund_key = fund.replace('_', ' ')
|
|
1907
|
+
fund_location = self.get_table("funds").query("Fund == @fund_key")["Location"].squeeze()
|
|
1908
|
+
currency_location = "us" if currency == "usd" else "bz"
|
|
1909
|
+
if fund_location != currency_location:
|
|
1910
|
+
usdbrl = self.get_prices(usdbrl_ticker, previous_date=cash["Date"].min(), recent_date=cash["Date"].max(),)
|
|
1911
|
+
usdbrl = self.usdbrl_clean_coupon_fix(usdbrl)
|
|
1912
|
+
usdbrl["Date"] = usdbrl["Date"].dt.date
|
|
1913
|
+
usdbrl = usdbrl.rename(columns={"Last_Price":"usdbrl"})
|
|
1914
|
+
cash = cash.merge(usdbrl[["Date", "usdbrl"]], on="Date", how="left")
|
|
1915
|
+
if fund_location == "bz":
|
|
1916
|
+
cash["Market_Value"] = cash["Market_Value"] / cash["usdbrl"]
|
|
1917
|
+
else:
|
|
1918
|
+
cash["Market_Value"] = cash["Market_Value"] * cash["usdbrl"]
|
|
1919
|
+
|
|
1920
|
+
# Removendo coluna auxiliar
|
|
1921
|
+
cash = cash.drop(columns=["usdbrl"])
|
|
1922
|
+
|
|
1923
|
+
# Vamos calcular o preco de fechamento com o rendimento do dia
|
|
1924
|
+
bz_cash_index = self.get_prices("bzacselc index", period="60m", currency=currency)
|
|
1925
|
+
bz_cash_index["Date"] = bz_cash_index["Date"].dt.date
|
|
1926
|
+
bz_cash_index = (bz_cash_index[["Date", "Last_Price"]]
|
|
1927
|
+
.rename(columns={"Last_Price":"bz_cash_index"})
|
|
1928
|
+
.set_index("Date").pct_change()
|
|
1929
|
+
.fillna(0)+1).reset_index()
|
|
1930
|
+
|
|
1931
|
+
us_cash_index = self.get_prices("sofrindx index", period="60m", currency=currency)
|
|
1932
|
+
us_cash_index["Date"] = us_cash_index["Date"].dt.date
|
|
1933
|
+
us_cash_index = (us_cash_index[["Date", "Last_Price"]]
|
|
1934
|
+
.rename(columns={"Last_Price":"us_cash_index"})
|
|
1935
|
+
.set_index("Date").pct_change()
|
|
1936
|
+
.fillna(0)+1).reset_index()
|
|
1937
|
+
|
|
1938
|
+
us_margin_cost = self.get_prices("sofr + 75bps", period="60m", currency=currency)
|
|
1939
|
+
us_margin_cost["Date"] = us_margin_cost["Date"].dt.date
|
|
1940
|
+
us_margin_cost = (us_margin_cost[["Date", "Last_Price"]]
|
|
1941
|
+
.rename(columns={"Last_Price":"us_margin_cost"})
|
|
1942
|
+
.set_index("Date").pct_change()
|
|
1943
|
+
.fillna(0)+1).reset_index()
|
|
1944
|
+
|
|
1945
|
+
cash = cash.merge(bz_cash_index, on="Date", how="left")
|
|
1946
|
+
cash = cash.merge(us_cash_index, on="Date", how="left")
|
|
1947
|
+
cash = cash.merge(us_margin_cost, on="Date", how="left")
|
|
1948
|
+
# Dependendo do ativo e da posicao a variacao no dia será diferente
|
|
1949
|
+
cash["Open_Price"] = 1
|
|
1950
|
+
cash["Close_Price"] = cash["bz_cash_index"]
|
|
1951
|
+
cash["Close_Price"] = np.where(cash["Group"] == "caixa_us",
|
|
1952
|
+
cash["us_cash_index"], cash["Close_Price"]
|
|
1953
|
+
)
|
|
1954
|
+
cash["Location"] = 'us'
|
|
1955
|
+
cash["Location"] = np.where(cash["Group"] == "caixa_us",
|
|
1956
|
+
'us', 'bz'
|
|
1957
|
+
)
|
|
1958
|
+
cash["Close_Price"] = np.where((cash["Market_Value"] < 0) & (cash["Group"] == "caixa_us") ,
|
|
1959
|
+
cash["us_margin_cost"], cash["Close_Price"]
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
cash = (cash[["Date", "Group", "Location", "Market_Value", "Open_Price", "Close_Price"]]
|
|
1963
|
+
.rename(columns={"Date":"Today",
|
|
1964
|
+
"Market_Value":"Close_Mkt_Value"
|
|
1965
|
+
}))
|
|
1966
|
+
# Criando colunas necessarias para concatenar com o df do daily pnl
|
|
1967
|
+
cash["Close_Quantity"] = cash["Close_Mkt_Value"]
|
|
1968
|
+
|
|
1969
|
+
cash["Type"] = cash["Group"]
|
|
1970
|
+
cash["Name"] = cash["Group"].str.replace("_", " ").str.title()
|
|
1971
|
+
cash["Asset_ID"] = cash["Type"]+"_"+cash["Type"]
|
|
1972
|
+
cash["Price_Key"] = cash["Type"]
|
|
1973
|
+
|
|
1974
|
+
cash["Delta_Shares"] = cash.groupby(["Group"])["Close_Quantity"].diff().fillna(0)
|
|
1975
|
+
cash["Shares_Sold"] = np.where(cash["Delta_Shares"] < 0, cash["Delta_Shares"], 0)
|
|
1976
|
+
cash["Amount_Sold"] = cash["Shares_Sold"]
|
|
1977
|
+
cash["Shares_Bought"] = np.where(cash["Delta_Shares"] > 0, cash["Delta_Shares"], 0)
|
|
1978
|
+
cash["Shares_Bought_Cost"] = cash["Shares_Bought"]
|
|
1979
|
+
|
|
1980
|
+
return cash
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
def __fix_open_price(self, df_op_fix, currency, ref_date, previous_date,
|
|
1984
|
+
usdbrl_ticker='bmfxclco curncy'):
|
|
1985
|
+
if len(df_op_fix) == 0:
|
|
1986
|
+
return pd.DataFrame()
|
|
1987
|
+
# Guardando a lista de colunas, para remover colunas auxiliares posteriormente
|
|
1988
|
+
columns_list= list(df_op_fix.columns)
|
|
1989
|
+
|
|
1990
|
+
asset_ids = df_op_fix["Asset_ID"].unique()
|
|
1991
|
+
assets = self.get_table("assets").query("Key.isin(@asset_ids)")\
|
|
1992
|
+
.copy()[["Key", "Ticker", "Currency Exposure"]].rename(columns={
|
|
1993
|
+
"Key":"Asset_ID", "Currency Exposure" : "Currency_Exposure"
|
|
1994
|
+
})
|
|
1995
|
+
|
|
1996
|
+
df_op_fix = df_op_fix.merge(assets, on='Asset_ID')
|
|
1997
|
+
df_op_fix["Needs_Fix"] = df_op_fix["Currency_Exposure"] != currency
|
|
1998
|
+
|
|
1999
|
+
df_fixed = df_op_fix.query("~Needs_Fix").copy()
|
|
2000
|
+
df_op_fix = df_op_fix.query("Needs_Fix").copy()
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
local_prices = self.get_prices(df_op_fix["Ticker"].unique(), recent_date=ref_date,
|
|
2004
|
+
previous_date=previous_date, currency='local')
|
|
2005
|
+
local_prices["Open_Price"] = local_prices.groupby("Asset")["Last_Price"]\
|
|
2006
|
+
.shift(1).fillna(local_prices["Last_Price"])
|
|
2007
|
+
usdbrl = self.get_prices(usdbrl_ticker, recent_date=ref_date,
|
|
2008
|
+
previous_date=previous_date
|
|
2009
|
+
)[["Date", "Last_Price"]].rename(columns={"Last_Price":"usdbrl"})
|
|
2010
|
+
local_prices = local_prices.merge(usdbrl, on="Date").rename(
|
|
2011
|
+
columns={"Asset":"Ticker", "Date" : "Today"})
|
|
2012
|
+
|
|
2013
|
+
# Considera apenas caso de brl e usd TODO: tratar outros casos que venham a existir
|
|
2014
|
+
if currency == 'usd':
|
|
2015
|
+
local_prices["Open_Price"] /= local_prices["usdbrl"]
|
|
2016
|
+
else:
|
|
2017
|
+
local_prices["Open_Price"] *= local_prices['usdbrl']
|
|
2018
|
+
|
|
2019
|
+
# Substituindo Open_Price antigo pelo novo ja com a conversao correta de moeda
|
|
2020
|
+
df_op_fix = df_op_fix.drop(columns=["Open_Price"])
|
|
2021
|
+
df_op_fix = df_op_fix.merge(local_prices[["Today", "Ticker", "Open_Price"]], on=['Today', 'Ticker'])
|
|
2022
|
+
|
|
2023
|
+
df_fixed = pd.concat([df_fixed, df_op_fix])
|
|
2024
|
+
# Removendo as colunas auxiliares
|
|
2025
|
+
df_fixed = df_fixed[columns_list]
|
|
2026
|
+
|
|
2027
|
+
return df_fixed
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def calculate_daily_pnl(self, fund, ref_date, currency, holiday_location="all", bdays=10,
|
|
2032
|
+
group_filters=[], type_filters=[], classification_filters=[], location_filters=[], asset_filters=[],
|
|
2033
|
+
currency_exposure_filters=[], include_cash=True, include_cash_debt=False, ticker_filters=[], annual_adm_fee=0,
|
|
2034
|
+
ignore_small_pnl=True): #test_op_fix=False):
|
|
2035
|
+
""" Calcula o PnL do dia de cada ativo no fundo.
|
|
2036
|
+
(Nao inclui o PnL das taxas!).
|
|
2037
|
+
Args:
|
|
2038
|
+
fund (str): nome do fundo
|
|
2039
|
+
ref_date (str): data de referência do PnL
|
|
2040
|
+
currency (str): moeda (usd|brl)
|
|
2041
|
+
holiday_location (str, optional): refencia de feriados. Defaults to "all".
|
|
2042
|
+
bdays (int, optional): quantidade de dias uteis a serem considerados.
|
|
2043
|
+
Defaults to 10.
|
|
2044
|
+
"""
|
|
2045
|
+
# dia util anterior
|
|
2046
|
+
# Obtendo todas as posicoes e movimentacoes, dos ultimos dias
|
|
2047
|
+
previous_date = self.get_bday_offset(ref_date, -bdays,
|
|
2048
|
+
location=holiday_location)
|
|
2049
|
+
df = self.get_positions_and_movements(fund, previous_date=previous_date,
|
|
2050
|
+
recent_date=ref_date, currency=currency)
|
|
2051
|
+
assets = self.get_table("assets").rename(columns={"Key":"Asset_ID",
|
|
2052
|
+
"Currency Exposure":"Currency_Exposure"})
|
|
2053
|
+
|
|
2054
|
+
# Filtrando pelos ativos desejados
|
|
2055
|
+
if len(classification_filters) > 0:
|
|
2056
|
+
classification_filters = [x.lower() for x in classification_filters]
|
|
2057
|
+
assets = assets.query("Luxor_Classification.isin(@classification_filters)")
|
|
2058
|
+
elif len(group_filters) > 0:
|
|
2059
|
+
group_filters = [x.lower() for x in group_filters]
|
|
2060
|
+
assets = assets.query("Group.isin(@group_filters)")
|
|
2061
|
+
elif len(type_filters) > 0:
|
|
2062
|
+
type_filters = [x.lower() for x in type_filters]
|
|
2063
|
+
assets = assets.query("Type.isin(@type_filters)")
|
|
2064
|
+
elif len(location_filters) > 0:
|
|
2065
|
+
location_filters = [x.lower() for x in location_filters]
|
|
2066
|
+
assets = assets.query("Location.isin(@location_filters)")
|
|
2067
|
+
elif len(asset_filters) > 0:
|
|
2068
|
+
asset_filters = [x.lower() for x in asset_filters]
|
|
2069
|
+
assets = assets.query("Asset.isin(@asset_filters)")
|
|
2070
|
+
elif len(ticker_filters) > 0:
|
|
2071
|
+
ticker_filters = [x.lower() for x in ticker_filters]
|
|
2072
|
+
assets = assets.query("Ticker.isin(@ticker_filters)")
|
|
2073
|
+
elif len(currency_exposure_filters) > 0:
|
|
2074
|
+
currency_exposure_filters = [x.lower() for x in currency_exposure_filters]
|
|
2075
|
+
assets = assets.query("Currency_Exposure.isin(@currency_exposure_filters)")
|
|
2076
|
+
# Fazemos uma juncao interna, mantendo somente as linhas filtradas acima
|
|
2077
|
+
df = df.merge(assets[["Asset_ID", "Name", "Group", "Type", "Location",
|
|
2078
|
+
"Luxor_Classification"]], on="Asset_ID")
|
|
2079
|
+
|
|
2080
|
+
df = df.sort_values(by="Date", ascending=True)
|
|
2081
|
+
df = df.rename(columns={
|
|
2082
|
+
"Date" : "Today",
|
|
2083
|
+
"Open_Quantity" : "Close_Quantity",
|
|
2084
|
+
"Last_Price" : "Close_Price",
|
|
2085
|
+
"Current_Position_Value" : "Close_Mkt_Value"
|
|
2086
|
+
})
|
|
2087
|
+
|
|
2088
|
+
# Obtendo o preco de abertura
|
|
2089
|
+
df["Open_Price"] = df.groupby("Asset_ID")["Close_Price"].shift(1).fillna(df["Close_Price"])
|
|
2090
|
+
types_to_fix_open_price = ['ndf usdbrl']
|
|
2091
|
+
#if test_op_fix:
|
|
2092
|
+
|
|
2093
|
+
df_op_fix = df.query("Type.isin(@types_to_fix_open_price)").copy()
|
|
2094
|
+
df_op_fix = self.__fix_open_price(df_op_fix, currency, ref_date, previous_date)
|
|
2095
|
+
|
|
2096
|
+
df = df.query("~Type.isin(@types_to_fix_open_price)").copy()
|
|
2097
|
+
|
|
2098
|
+
# Juntando dfs novamente
|
|
2099
|
+
df = pd.concat([df, df_op_fix])
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
# Precisamos consertar o open_price do FX. (Deve ser convertido pelo spot de fechamento sempre)
|
|
2104
|
+
|
|
2105
|
+
# Podemos puxar o caixa aqui e concatenar para as proximas operacoes
|
|
2106
|
+
if include_cash:
|
|
2107
|
+
cash = self.get_hist_cash_movements(fund, ref_date, currency=currency, bdays=bdays, holiday_location=holiday_location)
|
|
2108
|
+
cash["Today"] = pd.to_datetime(cash["Today"])
|
|
2109
|
+
cash = cash.query("Close_Mkt_Value >= 0").copy()
|
|
2110
|
+
cash["Luxor_Classification"] = 'fixed income'
|
|
2111
|
+
|
|
2112
|
+
if len(location_filters) > 0:
|
|
2113
|
+
location_filters = [x.lower() for x in location_filters]
|
|
2114
|
+
cash = cash.query("Location.isin(@location_filters)").copy()
|
|
2115
|
+
|
|
2116
|
+
df = pd.concat([df, cash])
|
|
2117
|
+
df = df.sort_values(by="Today", ascending=True)
|
|
2118
|
+
|
|
2119
|
+
# Vamos segregar como dívida o caixa virado.
|
|
2120
|
+
if include_cash_debt:
|
|
2121
|
+
cash_debt = self.get_hist_cash_movements(fund, ref_date, currency=currency, bdays=bdays, holiday_location=holiday_location)
|
|
2122
|
+
cash_debt["Today"] = pd.to_datetime(cash_debt["Today"])
|
|
2123
|
+
cash_debt = cash_debt.query("Close_Mkt_Value < 0").copy()
|
|
2124
|
+
cash_debt["Luxor_Classification"] = 'debt'
|
|
2125
|
+
|
|
2126
|
+
# Caixa virado no Brasil, vamos desconsiderar, pois na prática nao teremos.
|
|
2127
|
+
#cash_debt = cash_debt.query("Asset_ID != 'caixa_caixa' or Location != 'bz'").copy()
|
|
2128
|
+
#Optando por manter e retirar o PnL posteriormente, para nao perder a contribuicao dele pra aporte e resgate
|
|
2129
|
+
|
|
2130
|
+
if len(location_filters) > 0:
|
|
2131
|
+
location_filters = [x.lower() for x in location_filters]
|
|
2132
|
+
cash_debt = cash_debt.query("Location.isin(@location_filters)").copy()
|
|
2133
|
+
|
|
2134
|
+
df = pd.concat([df, cash_debt])
|
|
2135
|
+
df = df.sort_values(by="Today", ascending=True)
|
|
2136
|
+
|
|
2137
|
+
# Vamos obter a data d-1
|
|
2138
|
+
df["Yesterday"] = (df.groupby("Asset_ID")["Today"].shift(1)
|
|
2139
|
+
.fillna(df["Today"]-dt.timedelta(days=1))
|
|
2140
|
+
)
|
|
2141
|
+
# Obtendo quantidade na abertura
|
|
2142
|
+
df["Open_Quantity"] = df.groupby("Asset_ID")["Close_Quantity"].shift(1).fillna(0)
|
|
2143
|
+
|
|
2144
|
+
# Obtendo valor de mercado por ativo na abertura
|
|
2145
|
+
df["Open_Mkt_Value"] = df["Open_Quantity"] * df["Open_Price"]
|
|
2146
|
+
# Calculando precos de compas e vendas
|
|
2147
|
+
df["Buy_Price"] = np.where(df["Shares_Bought"] > 0,
|
|
2148
|
+
abs(df["Shares_Bought_Cost"]/df["Shares_Bought"]),0)
|
|
2149
|
+
df["Sell_Price"] = np.where(df["Shares_Sold"] < 0,
|
|
2150
|
+
abs(df["Amount_Sold"]/df["Shares_Sold"]), 0)
|
|
2151
|
+
|
|
2152
|
+
# Calculando PnL Diário (Caixa nao pode ser calculado aqui, pois o preco eh sempre 1)
|
|
2153
|
+
df["Pnl_Bought"] = abs(df["Shares_Bought"]) * (df["Close_Price"] - df["Buy_Price"])
|
|
2154
|
+
df["Pnl_Sold"] = abs(df["Shares_Sold"]) * (df["Sell_Price"] - df["Open_Price"])
|
|
2155
|
+
# Ajustando PnL sold. Quando for debt, a venda representa a divida sendo tomada
|
|
2156
|
+
# e a compra representa o pagamento da divida. Na primeira, ha juros e na segunda nao ha pnl.
|
|
2157
|
+
df['Pnl_Bought'] = np.where(df['Luxor_Classification'] == 'debt', 0, df['Pnl_Bought'])
|
|
2158
|
+
df['Pnl_Sold'] = np.where(df['Luxor_Classification'] == 'debt',
|
|
2159
|
+
(df['Open_Price']-df['Close_Price'])*abs(df['Shares_Sold']), 0)
|
|
2160
|
+
|
|
2161
|
+
df["Pnl_Unchanged"] = (df["Open_Quantity"] + df["Shares_Sold"]) * (df["Close_Price"] - df["Open_Price"])
|
|
2162
|
+
|
|
2163
|
+
# O caixa no BZ pode estar virado temporariamente durante aportes/resgates de caixa_us, mas isso n estará gerando PnL
|
|
2164
|
+
df["Pnl_Unchanged"] = np.where((df["Asset_ID"] == 'caixa_caixa') & (df["Location"] == "bz") & (df["Location"] == "bz")& (df["Close_Mkt_Value"] < 0), 0, df["Pnl_Unchanged"])
|
|
2165
|
+
df["Pnl_Sold"] = np.where((df["Asset_ID"] == 'caixa_caixa') & (df["Location"] == "bz") & (df["Close_Mkt_Value"] < 0), 0, df["Pnl_Sold"])
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
# Ajustar aqui as operacoes com logica de pnl e exposicao
|
|
2170
|
+
# Para nao considerar no AUM e no Cash Flow
|
|
2171
|
+
types_to_recalculate = ['ndf usdbrl']
|
|
2172
|
+
# Separando os dados em dois grupos, para editar um deles
|
|
2173
|
+
df_exposure = df.query("Type.isin(@types_to_recalculate)").copy()
|
|
2174
|
+
|
|
2175
|
+
df = df.query("~Type.isin(@types_to_recalculate)").copy()
|
|
2176
|
+
# Calculo especifico para pnl do FX.
|
|
2177
|
+
df_exposure["Total_Pnl"] = df_exposure["Pnl_Bought"] + df_exposure["Pnl_Sold"] \
|
|
2178
|
+
+ df_exposure["Pnl_Unchanged"]
|
|
2179
|
+
|
|
2180
|
+
df_exposure = df_exposure.set_index(["Asset_ID"])
|
|
2181
|
+
# Valor de mercado sera o PnL acumulado
|
|
2182
|
+
df_exposure["Total_Pnl"] = df_exposure.groupby(["Asset_ID"])["Total_Pnl"].cumsum()
|
|
2183
|
+
df_exposure = df_exposure.reset_index()
|
|
2184
|
+
df_exposure["Close_Mkt_Value"] = df_exposure["Total_Pnl"]
|
|
2185
|
+
df_exposure["Open_Mkt_Value"] = df_exposure["Close_Mkt_Value"].shift(1, fill_value=0)
|
|
2186
|
+
|
|
2187
|
+
# Retirando qualquer possibilidade de impacto para aporte e resgate
|
|
2188
|
+
df_exposure["Shares_Bought_Cost"] = 0
|
|
2189
|
+
df_exposure["Amount_Sold"] = 0
|
|
2190
|
+
# TODO Pensar o que vai mudar no caso de uma zeragem parcial
|
|
2191
|
+
df_exposure = df_exposure.drop(columns="Total_Pnl")
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
# Juntando dfs novamente
|
|
2195
|
+
df = pd.concat([df, df_exposure])
|
|
2196
|
+
|
|
2197
|
+
# Calculando net de aporte e resgate por ativo
|
|
2198
|
+
df["Cash_Flow"] = abs(df["Shares_Bought_Cost"]) + -abs(df["Amount_Sold"])
|
|
2199
|
+
|
|
2200
|
+
hist_dividends = self.get_dividends(fund, ref_date, previous_date, currency=currency,
|
|
2201
|
+
holiday_location=holiday_location)
|
|
2202
|
+
hist_dividends = pd.merge(assets[["Asset_ID", "Ticker"]],
|
|
2203
|
+
hist_dividends, on="Ticker",how="left")[["Date", "Asset_ID", "Amount"]]
|
|
2204
|
+
hist_dividends = hist_dividends.rename(columns={"Date": "Today", "Amount":"Dividends"})
|
|
2205
|
+
|
|
2206
|
+
df = df.merge(hist_dividends, on=["Today", "Asset_ID"], how="left")
|
|
2207
|
+
df["Dividends"] = df["Dividends"].fillna(0)
|
|
2208
|
+
|
|
2209
|
+
|
|
2210
|
+
df["Net_Subscriptions_Redemptions"] = df.groupby("Today")["Cash_Flow"].sum()
|
|
2211
|
+
|
|
2212
|
+
df = df[["Today", "Name", "Asset_ID", "Group", "Type", "Location", "Luxor_Classification",
|
|
2213
|
+
"Open_Quantity", "Open_Mkt_Value", "Close_Quantity",
|
|
2214
|
+
"Shares_Bought", "Buy_Price", "Shares_Sold", "Sell_Price",
|
|
2215
|
+
"Dividends", "Close_Price", "Open_Price", "Close_Mkt_Value",
|
|
2216
|
+
"Cash_Flow", "Pnl_Bought", "Pnl_Sold", "Net_Subscriptions_Redemptions", "Pnl_Unchanged"]]
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
# Calculando AUM no fechamento
|
|
2220
|
+
df = df.set_index("Today")
|
|
2221
|
+
df["Close_AUM"] = df.groupby("Today")["Close_Mkt_Value"].sum()
|
|
2222
|
+
df = df.reset_index()
|
|
2223
|
+
|
|
2224
|
+
if annual_adm_fee != 0:
|
|
2225
|
+
# Incluir taxas de adm e gestao no calculo do PnL
|
|
2226
|
+
# Elas ja impactam o caixa, mas precisam ser consideradas no PnL
|
|
2227
|
+
daily_adm_fee = (1+abs(annual_adm_fee))**(1/365)-1
|
|
2228
|
+
df_fees = df[["Today", "Close_AUM"]].groupby("Today").last()
|
|
2229
|
+
df_fees["Pnl_Unchanged"] = -df_fees["Close_AUM"] * daily_adm_fee
|
|
2230
|
+
df_fees = df_fees.reset_index()
|
|
2231
|
+
df_fees["Asset_ID"] = "taxas e custos_tx adm"
|
|
2232
|
+
|
|
2233
|
+
value_columns = df.columns[7:-2] # Sobrescreve com 0, menos para Pnl_Unchanged e Close_AUM
|
|
2234
|
+
|
|
2235
|
+
df_fees[value_columns] = 0 # Para taxas, todos os outros valores nao importam.
|
|
2236
|
+
# Finalmente, falta colocar dados de name, type e group
|
|
2237
|
+
all_assets = self.get_table("assets").rename(columns={"Key":"Asset_ID"})
|
|
2238
|
+
|
|
2239
|
+
df_fees = df_fees.merge(all_assets[["Asset_ID", "Name","Type", "Group", "Location"]], on="Asset_ID")
|
|
2240
|
+
if len(location_filters) > 0:
|
|
2241
|
+
location_filters = [x.lower() for x in location_filters]
|
|
2242
|
+
df_fees = df_fees.query("Location.isin(@location_filters)")
|
|
2243
|
+
|
|
2244
|
+
df = pd.concat([df, df_fees])
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
df["Daily_Pnl"] = df["Pnl_Unchanged"] + df["Pnl_Bought"] + df["Pnl_Sold"] + df["Dividends"]
|
|
2248
|
+
|
|
2249
|
+
# Calculando PL Inicial Ajustado
|
|
2250
|
+
df = df.set_index("Today")
|
|
2251
|
+
df["Net_Subscriptions_Redemptions"] = df.reset_index().groupby("Today")["Cash_Flow"].sum()
|
|
2252
|
+
# Sera o valor de mercado da abertura somado com o net de aportes.
|
|
2253
|
+
# Racional: Resgates rentabilizam no dia (ja estao no inicial)
|
|
2254
|
+
# Aportes rentabilizam no dia (nao estano no inicial)
|
|
2255
|
+
df["Open_AUM"] = df.reset_index().groupby("Today")["Open_Mkt_Value"].sum()
|
|
2256
|
+
df["Open_AUM_Adjusted"] = (df["Open_AUM"] +
|
|
2257
|
+
df["Net_Subscriptions_Redemptions"]*(df["Net_Subscriptions_Redemptions"] > 0)
|
|
2258
|
+
)
|
|
2259
|
+
|
|
2260
|
+
df["Daily_Attribution"] = df["Daily_Pnl"] / df["Open_AUM_Adjusted"]
|
|
2261
|
+
df["Daily_Return"] = df.reset_index().groupby("Today")["Daily_Attribution"].sum()
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
# ao retornar, filtrar port Daily_Pnl != 0 e not Daily_Pnl.isna()
|
|
2265
|
+
if ignore_small_pnl:
|
|
2266
|
+
return (df.reset_index()
|
|
2267
|
+
.sort_values(by="Today").query(''' (Daily_Pnl <= -0.01 or Daily_Pnl >= 0.01)\
|
|
2268
|
+
and not Daily_Pnl.isna()'''))
|
|
2269
|
+
return df.reset_index().sort_values(by="Today")
|
|
2270
|
+
|
|
2271
|
+
#return df.reset_index().sort_values(by="Today")
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def get_dividends(self, fund_name, recent_date, previous_date=None, period=None, currency='usd',
|
|
2275
|
+
usdbrl_ticker='bmfxclco curncy', holiday_location='all'):
|
|
2276
|
+
|
|
2277
|
+
if period is not None:
|
|
2278
|
+
previous_date = self.get_start_period_dt(recent_date, period=period, holiday_location=holiday_location)
|
|
2279
|
+
|
|
2280
|
+
df = self.get_table("hist_dividends").query("Fund == @fund_name and Date >= @previous_date and Date <= @recent_date").copy()
|
|
2281
|
+
df = df.reset_index(drop=True)
|
|
2282
|
+
if currency == 'local':
|
|
2283
|
+
return df
|
|
2284
|
+
|
|
2285
|
+
df_usd = df.query("Currency == 'usd'").copy()
|
|
2286
|
+
df_brl = df.query("Currency == 'brl'").copy()
|
|
2287
|
+
|
|
2288
|
+
if currency == 'usd':
|
|
2289
|
+
df_brl_mod = self.convert_currency(df_brl[["Date", "Amount"]].rename(columns={"Amount":"Last_Price"}),
|
|
2290
|
+
price_currency="brl",
|
|
2291
|
+
dest_currency="usd"
|
|
2292
|
+
).rename(columns={'Last_Price' : 'Amount'})
|
|
2293
|
+
df_brl.loc[df_brl_mod.index, 'Amount'] = df_brl_mod['Amount']
|
|
2294
|
+
|
|
2295
|
+
if currency == 'brl':
|
|
2296
|
+
df_usd_mod = self.convert_currency(df_usd[["Date", "Amount"]].rename(columns={"Amount":"Last_Price"}),
|
|
2297
|
+
price_currency="usd",
|
|
2298
|
+
dest_currency="brl"
|
|
2299
|
+
).rename(columns={'Last_Price':'Amount'})
|
|
2300
|
+
df_usd.loc[df_usd_mod.index, 'Amount'] = df_usd_mod['Amount']
|
|
2301
|
+
|
|
2302
|
+
df = pd.concat([df_brl, df_usd])[['Date', 'Ticker', 'Amount']]
|
|
2303
|
+
|
|
2304
|
+
if fund_name == 'lipizzaner':
|
|
2305
|
+
df_fund_a = self.get_dividends('fund a', recent_date, previous_date, period, currency, usdbrl_ticker, holiday_location)
|
|
2306
|
+
df = pd.concat([df, df_fund_a])
|
|
2307
|
+
|
|
2308
|
+
return df
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
def get_position_variation(self, fund_name, recent_date, previous_date):
|
|
2313
|
+
"""
|
|
2314
|
+
Fornece um dataframe com a variacao das posicoes do fundo entre as datas.
|
|
2315
|
+
Inclui tambem uma coluna de flag informando se a posicao foi zerada ou nao
|
|
2316
|
+
|
|
2317
|
+
"""
|
|
2318
|
+
|
|
2319
|
+
# Pegamos as posicoes da data mais recente, transformando de dict para dataframe
|
|
2320
|
+
cur_positions = self.get_positions(fund_name, recent_date)
|
|
2321
|
+
cur_positions = pd.DataFrame(cur_positions.items(), columns = ["Key", "#_cur"])
|
|
2322
|
+
|
|
2323
|
+
# Igual para as posicoes da data mais antiga
|
|
2324
|
+
prev_positions = self.get_positions(fund_name, previous_date)
|
|
2325
|
+
prev_positions = pd.DataFrame(prev_positions.items(), columns=["Key", "#_prev"])
|
|
2326
|
+
|
|
2327
|
+
# Realizamos juncao externa na chave, mantendo assim dados nan (zeragens e ativos novos)
|
|
2328
|
+
diff = pd.merge(cur_positions, prev_positions, on="Key", how="outer").fillna(0)
|
|
2329
|
+
|
|
2330
|
+
diff["Variation"] = diff["#_cur"] - diff["#_prev"]
|
|
2331
|
+
diff["Closed"] = diff["#_cur"] == 0
|
|
2332
|
+
|
|
2333
|
+
return diff[["Key", "Variation", "Closed"]]
|
|
2334
|
+
|
|
2335
|
+
|
|
2336
|
+
def get_risk_metric(self, fund_name, metrics=None, date=dt.date.today()):
|
|
2337
|
+
"""
|
|
2338
|
+
Retorna as metricas de risco numa determinada data.
|
|
2339
|
+
|
|
2340
|
+
Args:
|
|
2341
|
+
fund_name (str): nome do fundo correspontente
|
|
2342
|
+
metrics (str, list(str)): str da metrica desejada ou uma lista de strings
|
|
2343
|
+
para mais de uma metrica. Defaults to None (all available).
|
|
2344
|
+
date (datetime.date, optional): Data da metrica. Defaults to datetime.date.today().
|
|
2345
|
+
|
|
2346
|
+
Returns:
|
|
2347
|
+
|
|
2348
|
+
Pode retornar o valor de uma metrica especifica ou Dataframe quando
|
|
2349
|
+
metrics passado for uma lista de metricas.
|
|
2350
|
+
|
|
2351
|
+
"""
|
|
2352
|
+
fund_name = fund_name.replace("_", " ")
|
|
2353
|
+
|
|
2354
|
+
hist_metrics = self.get_table("hist_risk_metrics")
|
|
2355
|
+
hist_metrics = hist_metrics.loc[((hist_metrics["Fund"] == fund_name) & (hist_metrics["Date"].dt.date <= date) )].tail(1).reset_index(drop=True)
|
|
2356
|
+
|
|
2357
|
+
if metrics is None:
|
|
2358
|
+
metrics = list(hist_metrics.columns)
|
|
2359
|
+
metrics.remove("Date")
|
|
2360
|
+
metrics.remove("Fund")
|
|
2361
|
+
|
|
2362
|
+
try:
|
|
2363
|
+
if type(metrics) == list:
|
|
2364
|
+
|
|
2365
|
+
return hist_metrics[metrics].astype(float)
|
|
2366
|
+
|
|
2367
|
+
return float(hist_metrics[metrics].squeeze())
|
|
2368
|
+
except KeyError:
|
|
2369
|
+
logger.error(f"Metrica(s) {metrics} indisponivel(is)")
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
def get_risk_metric_variation(self, fund_name, recent_date, previous_date, metrics=None):
|
|
2373
|
+
|
|
2374
|
+
recent = self.get_risk_metric(fund_name, date=recent_date, metrics=metrics)
|
|
2375
|
+
previous = self.get_risk_metric(fund_name, date=previous_date, metrics=metrics)
|
|
2376
|
+
|
|
2377
|
+
return recent - previous
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
def get_group_results(self, previous_date= None, recent_date = None, segments=None, currency="brl", inception_dates=None, period = None) -> pd.DataFrame:
|
|
2381
|
+
"""Calcula e retorna o resultado de cada segmento.
|
|
2382
|
+
|
|
2383
|
+
Args:
|
|
2384
|
+
recent_date (dt.date): Data inicial NAO inclusiva (sera filtrado por uma data anterior a essa.)
|
|
2385
|
+
Ex. Para considerar retorno ate o mes de marco, deve ser passada alguma data em abril.
|
|
2386
|
+
previous_date (dt.date): Data final
|
|
2387
|
+
segments (_type_, optional): Lista ou string com nome dos segmentos
|
|
2388
|
+
desejados. Defaults to None. Quando None, retorna todos os segmentos existentes.
|
|
2389
|
+
|
|
2390
|
+
Returns:
|
|
2391
|
+
pd.DataFrame: Retorno por segmento.
|
|
2392
|
+
"""
|
|
2393
|
+
|
|
2394
|
+
if (previous_date is not None) and (previous_date > recent_date):
|
|
2395
|
+
logger.error("Data inicial é menor que a data final. Parametros invertidos?")
|
|
2396
|
+
sys.exit()
|
|
2397
|
+
|
|
2398
|
+
# Obtendo tabela de retornos historicos extraida do retorno consolidado
|
|
2399
|
+
group_results = self.get_table("luxor group results")[["Date", "Segment", "Return_Multiplier"]].copy()
|
|
2400
|
+
group_results["Date"] = pd.to_datetime(group_results["Date"])
|
|
2401
|
+
|
|
2402
|
+
# Selecionando o periodo de tempo desejado
|
|
2403
|
+
if period is not None:
|
|
2404
|
+
recent_date = dt.date(recent_date.year,recent_date.month,20)+dt.timedelta(days = 15)
|
|
2405
|
+
previous_date = self.get_start_period_dt(recent_date, period=period)
|
|
2406
|
+
if period.lower() == "ytd":
|
|
2407
|
+
if recent_date.month == 1: # fechamento de dezembro! Vamos ter que ajustar o previous_date
|
|
2408
|
+
previous_date = self.get_start_period_dt(recent_date-dt.timedelta(days=35), period=period)
|
|
2409
|
+
previous_date += dt.timedelta(days=1)
|
|
2410
|
+
|
|
2411
|
+
group_results = group_results.query("Date >= @previous_date and Date < @recent_date").sort_values(by=("Date")).copy()
|
|
2412
|
+
|
|
2413
|
+
if segments is not None:
|
|
2414
|
+
# Vamos deixar apenas os segmentos informados no parametro 'segments'
|
|
2415
|
+
if type(segments) is str:
|
|
2416
|
+
segments = [segments]
|
|
2417
|
+
if type(segments) is list:
|
|
2418
|
+
seg_filter = segments
|
|
2419
|
+
else:
|
|
2420
|
+
logger.error("'segments' precisa ser do tipo 'list' ou 'str'")
|
|
2421
|
+
sys.exit()
|
|
2422
|
+
|
|
2423
|
+
group_results = group_results.query(" Segment.isin(@seg_filter)").copy()
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
group_results = group_results.set_index("Date").groupby("Segment").prod() -1
|
|
2427
|
+
group_results = group_results.reset_index()
|
|
2428
|
+
|
|
2429
|
+
# Retornos dos segmentos, por padrao, estarao em R$.
|
|
2430
|
+
# Para ver em US$ precisa ser feita a conversao.
|
|
2431
|
+
if currency == "usd":
|
|
2432
|
+
|
|
2433
|
+
def __get_usdbrl_variation(segment):
|
|
2434
|
+
# ajustando janelas de inicio e fim, para pegar a variacao correta de usd no periodo -> A base do grupo eh de retornos, a de usd eh de preços
|
|
2435
|
+
usd_recent_date = dt.date(recent_date.year, recent_date.month, 1) - dt.timedelta(days=1)
|
|
2436
|
+
usd_previous_date = dt.date(previous_date.year, previous_date.month, 1) -dt.timedelta(days=1)
|
|
2437
|
+
# Ainda, se a data for anterior ou igual ao inceptio date, vamos usar o incepion date
|
|
2438
|
+
#print(f"conversao usdbrl usando: previous_date:{usd_previous_date} recent_date: {usd_recent_date}")
|
|
2439
|
+
if inception_dates is not None and segment in inception_dates.keys():
|
|
2440
|
+
|
|
2441
|
+
sgmnt_inception = inception_dates[segment] -dt.timedelta(days=1) # subtraindo 1 dia para usar o fechamento do dia anterior ao inicio
|
|
2442
|
+
usd_previous_date = sgmnt_inception if ((sgmnt_inception.year == previous_date.year) and (sgmnt_inception.month == previous_date.month)) else usd_previous_date
|
|
2443
|
+
#"bmfxclco curncy" -> converter usando usd cupom limpo
|
|
2444
|
+
|
|
2445
|
+
delta_usdbrl = self.get_pct_change("bmfxclco curncy", previous_date=usd_previous_date, recent_date=usd_recent_date)
|
|
2446
|
+
#print(f"Segmento: {segment} Retorno_BRL:{multiplier} delta usdbrl={delta_usdbrl} Retorno_USD: {(1+multiplier)/(1+delta_usdbrl)-1}")
|
|
2447
|
+
return delta_usdbrl
|
|
2448
|
+
|
|
2449
|
+
|
|
2450
|
+
# Convertendo para usd o que estiver em brl
|
|
2451
|
+
|
|
2452
|
+
group_results["Return_Multiplier"] = group_results.apply(lambda row: (1 + row["Return_Multiplier"])/(1+__get_usdbrl_variation(row["Segment"])) -1, axis=1)
|
|
2453
|
+
|
|
2454
|
+
group_results = group_results.set_index("Segment")
|
|
2455
|
+
|
|
2456
|
+
return group_results
|
|
2457
|
+
|
|
2458
|
+
def calculate_fund_particip(self, fund_name, fund_owned, date):
|
|
2459
|
+
"""
|
|
2460
|
+
Informa o percentual que 'fund_name' possui do 'fund_owned'.
|
|
2461
|
+
|
|
2462
|
+
Args:
|
|
2463
|
+
fund_name (str)
|
|
2464
|
+
fund_owned (str)
|
|
2465
|
+
date (dt.date)
|
|
2466
|
+
Returns:
|
|
2467
|
+
(float)
|
|
2468
|
+
"""
|
|
2469
|
+
fund_name = fund_name.replace("_", " ")
|
|
2470
|
+
fund_owned_key = fund_owned + "_" + fund_owned
|
|
2471
|
+
|
|
2472
|
+
# antes da data de incorporacao do manga master pelo lipi, 100% do manga master era do manga fic
|
|
2473
|
+
if fund_name == "mangalarga fic fia" and fund_owned == "mangalarga master":
|
|
2474
|
+
if date < dt.date(2022,12,9):
|
|
2475
|
+
return 1.0
|
|
2476
|
+
else: return 0
|
|
2477
|
+
# antes da data de incorporacao do manga master pelo lipi, 100% do fund_a era do manga master
|
|
2478
|
+
if fund_name == "mangalarga master" and fund_owned == "fund a":
|
|
2479
|
+
if date < dt.date(2022,12,9):
|
|
2480
|
+
return 1.0
|
|
2481
|
+
else: return 0
|
|
2482
|
+
# Adicionando mais um nivel de calculo
|
|
2483
|
+
if fund_name == "mangalarga fic fia" and fund_owned == "fund a":
|
|
2484
|
+
if date < dt.date(2022,12,9):
|
|
2485
|
+
return self.calculate_fund_particip(fund_name, "mangalarga master", date) * self.calculate_fund_particip("mangalarga master", fund_owned, date)
|
|
2486
|
+
else:
|
|
2487
|
+
return self.calculate_fund_particip(fund_name, "lipizzaner", date) * self.calculate_fund_particip("lipizzaner", fund_owned, date)
|
|
2488
|
+
|
|
2489
|
+
if fund_name == "mangalarga ii fic fia" and fund_owned == "fund a":
|
|
2490
|
+
if date < dt.date(2023,2,7):
|
|
2491
|
+
return self.calculate_fund_particip("mangalarga fic fia", "mangalarga master", date)
|
|
2492
|
+
else:
|
|
2493
|
+
return self.calculate_fund_particip(fund_name, "lipizzaner", date) * self.calculate_fund_particip("lipizzaner", fund_owned, date)
|
|
2494
|
+
|
|
2495
|
+
position = self.get_table("hist_positions")
|
|
2496
|
+
|
|
2497
|
+
# Busca otimizada pela posicao
|
|
2498
|
+
try:
|
|
2499
|
+
position = (position["#"].to_numpy()[(
|
|
2500
|
+
(position["Fund"].to_numpy() == fund_name)
|
|
2501
|
+
&
|
|
2502
|
+
(position["Asset_ID"].to_numpy() == fund_owned_key)
|
|
2503
|
+
&
|
|
2504
|
+
(position["Date"].dt.date.to_numpy() <= date)
|
|
2505
|
+
)].item(-1))
|
|
2506
|
+
except (IndexError , KeyError):
|
|
2507
|
+
# nao havia participacao do fundo em questao na data informada
|
|
2508
|
+
return 0
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
fund_owned_total_amount = self.get_table("all_funds_quotas")
|
|
2512
|
+
|
|
2513
|
+
try:
|
|
2514
|
+
fund_owned_total_amount = (fund_owned_total_amount["#"].to_numpy()[(
|
|
2515
|
+
(fund_owned_total_amount["Fund"].to_numpy() == fund_owned)
|
|
2516
|
+
&
|
|
2517
|
+
(fund_owned_total_amount["Date"].dt.date.to_numpy() <= date)
|
|
2518
|
+
)].item(-1))
|
|
2519
|
+
except (IndexError , KeyError):
|
|
2520
|
+
|
|
2521
|
+
# Algo deu errado
|
|
2522
|
+
return None
|
|
2523
|
+
|
|
2524
|
+
return round(position/fund_owned_total_amount, 5) # Mantem 5 casas decimais de precisao
|
|
2525
|
+
|
|
2526
|
+
def get_fund_aum(self, fund_name, date):
|
|
2527
|
+
|
|
2528
|
+
hist_quotas = self.get_table("all_funds_quotas").query("Fund == @fund_name")
|
|
2529
|
+
|
|
2530
|
+
return hist_quotas["#"].to_numpy()[hist_quotas["Date"].dt.date.to_numpy()==date].item(-1) * self.get_price(fund_name, px_date=date)
|
|
2531
|
+
|
|
2532
|
+
|
|
2533
|
+
def get_fund_numb_shares(self, fund_name, date):
|
|
2534
|
+
|
|
2535
|
+
hist_quotas = self.get_table(fund_name+"_quotas")
|
|
2536
|
+
return hist_quotas["#"].to_numpy()[hist_quotas["Date"].dt.date.to_numpy()==date].item(-1)
|
|
2537
|
+
|
|
2538
|
+
|
|
2539
|
+
def get_delta_subscription_redemption(self, fund, previous_date, recent_date):
|
|
2540
|
+
|
|
2541
|
+
hist_quotas = (self.get_table("all_funds_quotas")
|
|
2542
|
+
.query("Fund == @fund and Date>= @previous_date and Date <= @recent_date")
|
|
2543
|
+
.rename(columns={"#" : "Quota_Amnt"})[["Quota_Amnt", "Quota"]]
|
|
2544
|
+
)
|
|
2545
|
+
hist_quotas["Delta_Quotas"] = hist_quotas["Quota_Amnt"].diff().fillna(0)
|
|
2546
|
+
# Nao eh necessario usar a cota do dia anterior. Caso precise, basta fazer o shift abaixo
|
|
2547
|
+
# hist_quotas["Quota"] = hist_quotas["Quota"].shift(1).fillna(0)
|
|
2548
|
+
delta = (hist_quotas["Delta_Quotas"] * hist_quotas["Quota"]).sum()
|
|
2549
|
+
return delta
|
|
2550
|
+
|
|
2551
|
+
|
|
2552
|
+
def is_month_end(self, date):
|
|
2553
|
+
cur_m = date.month
|
|
2554
|
+
return (date + dt.timedelta(days=1)).month != cur_m
|
|
2555
|
+
|
|
2556
|
+
|
|
2557
|
+
def get_fund_pnl(self, fund, previous_date, recent_date): #, orig_curncy=None, dest_curncy=None):
|
|
2558
|
+
|
|
2559
|
+
# Nao ha nenhuma conversao de moeda a ser feita
|
|
2560
|
+
#if orig_curncy is None or dest_curncy is None or orig_curncy == dest_curncy:
|
|
2561
|
+
|
|
2562
|
+
start_aum = self.get_fund_aum(fund, date = previous_date)
|
|
2563
|
+
end_aum = self.get_fund_aum(fund, date = recent_date)
|
|
2564
|
+
period_cash_flow = self.get_delta_subscription_redemption(fund, previous_date, recent_date)
|
|
2565
|
+
|
|
2566
|
+
#if fund == "fund a":
|
|
2567
|
+
# print()
|
|
2568
|
+
# print(f"{fund}, prev:{previous_date} rec:{recent_date} pnl:{end_aum - start_aum - period_cash_flow} = {end_aum} - {start_aum} - {period_cash_flow}")
|
|
2569
|
+
|
|
2570
|
+
return end_aum - start_aum - period_cash_flow
|
|
2571
|
+
|
|
2572
|
+
# Desativando pois nao esta computando o resultado corretamente
|
|
2573
|
+
# Eh necessario converter de uma moeda para outra
|
|
2574
|
+
#location = self.get_table("funds").query("Fund == @fund")["Location"].squeeze()
|
|
2575
|
+
#end_date = recent_date
|
|
2576
|
+
#recent_date = previous_date
|
|
2577
|
+
#
|
|
2578
|
+
#total_pnl = 0
|
|
2579
|
+
#
|
|
2580
|
+
#while recent_date < end_date:
|
|
2581
|
+
#
|
|
2582
|
+
# recent_date = self.get_next_attr_date(fund, previous_date, location, mode="week")
|
|
2583
|
+
#
|
|
2584
|
+
# if recent_date > end_date:
|
|
2585
|
+
# recent_date = end_date
|
|
2586
|
+
#
|
|
2587
|
+
# # chamada recursiva, que roda apenas a primeira parte
|
|
2588
|
+
# pnl = self.get_fund_pnl(fund, previous_date, recent_date)
|
|
2589
|
+
#
|
|
2590
|
+
# end_usdbrl = self.get_price("bmfxclco curncy", px_date=recent_date)
|
|
2591
|
+
#
|
|
2592
|
+
# if orig_curncy == "brl" and dest_curncy =="usd":
|
|
2593
|
+
# total_pnl += pnl / end_usdbrl
|
|
2594
|
+
# elif orig_curncy == "usd" and dest_curncy == "brl":
|
|
2595
|
+
# total_pnl += pnl * end_usdbrl
|
|
2596
|
+
#
|
|
2597
|
+
# previous_date = recent_date
|
|
2598
|
+
#
|
|
2599
|
+
#return total_pnl
|
|
2600
|
+
|
|
2601
|
+
|
|
2602
|
+
def get_eduardo_lipi_particip(self, date=dt.date.today()):
|
|
2603
|
+
|
|
2604
|
+
funds_particip = (self.calculate_fund_particip("mangalarga fic fia", "lipizzaner", date=date)
|
|
2605
|
+
+ self.calculate_fund_particip("mangalarga ii fic fia", "lipizzaner", date=date)
|
|
2606
|
+
+ self.calculate_fund_particip("maratona", "lipizzaner", date=date)
|
|
2607
|
+
+ self.calculate_fund_particip("fund c", "lipizzaner", date=date))
|
|
2608
|
+
|
|
2609
|
+
if funds_particip > 1:
|
|
2610
|
+
print(" ----> Atenção! Calculo de participacao ficou errado para o eduardo. <----")
|
|
2611
|
+
|
|
2612
|
+
return 1 - funds_particip
|
|
2613
|
+
|
|
2614
|
+
|
|
2615
|
+
def get_next_attr_date(self, fund, start_date, location, mode): #funds_info):
|
|
2616
|
+
# Funcao auxiliar do attribution, permite pegar a proxima data valida para contabilizacao do attribution
|
|
2617
|
+
# Levar em consideracao que lipizzaner, antes de incorporar o manga, tinha também cota diaria.
|
|
2618
|
+
if mode == "day" or (fund == "lipizzaner" and start_date < dt.date(2022,12,9)):
|
|
2619
|
+
return self.get_bday_offset(date=start_date, offset=1, location=location)
|
|
2620
|
+
if mode == "week":
|
|
2621
|
+
thursday = 3 #weekday de quinta-feira (base da cota semanal)
|
|
2622
|
+
start_date_weekday = start_date.weekday()
|
|
2623
|
+
# Vamos setar a data como a proxima sexta e pegar o primeiro dia util anterior
|
|
2624
|
+
friday_offset = 8 if start_date_weekday == thursday else -(start_date_weekday - thursday - 1)
|
|
2625
|
+
next_date = self.get_bday_offset(date=start_date + dt.timedelta(days=friday_offset), offset=-1, location=location)
|
|
2626
|
+
|
|
2627
|
+
# Caso com muitos feriados proximos. Vamos resolver pegando o proximo dia util, para facilitar.
|
|
2628
|
+
if next_date <= start_date:
|
|
2629
|
+
return self.get_bday_offset(date=start_date, offset=1, location=location)
|
|
2630
|
+
|
|
2631
|
+
if next_date == dt.date(2023,1,12): next_date = dt.date(2023,1,16)
|
|
2632
|
+
|
|
2633
|
+
if next_date.month != start_date.month:
|
|
2634
|
+
# houve troca de mes. Se o ultimo dia do mes nao for start_date, vamos retornos o final do mes
|
|
2635
|
+
month_ending = dt.date(start_date.year, start_date.month, 15) + dt.timedelta(days=25)
|
|
2636
|
+
month_ending = dt.date(month_ending.year, month_ending.month, 1) - dt.timedelta(days=1)
|
|
2637
|
+
# mas se start_date for o month_ending, entao esta correto retornar next_date.
|
|
2638
|
+
if month_ending == start_date:
|
|
2639
|
+
return next_date
|
|
2640
|
+
return month_ending
|
|
2641
|
+
return next_date
|
|
2642
|
+
logger.critical("'mode' desconhecido.")
|
|
2643
|
+
|
|
2644
|
+
|
|
2645
|
+
def calculate_attribution(self, fund, previous_date, recent_date, fund_currency):
|
|
2646
|
+
""" Calcula o attribution do fundo informado para qualquer periodo.
|
|
2647
|
+
|
|
2648
|
+
Args:
|
|
2649
|
+
fund (str): nome do fundo (lipizzaner, fund a, fund b, ...)
|
|
2650
|
+
previous_date (datetime.date): data inicial
|
|
2651
|
+
recent_date (datetime.date): data final
|
|
2652
|
+
fund_currency (str): 'brl' ou 'usd
|
|
2653
|
+
|
|
2654
|
+
Returns:
|
|
2655
|
+
pandas.DataFrame: Dataframe contendo o attribution (% e pnl) por ativo no periodo.
|
|
2656
|
+
"""
|
|
2657
|
+
|
|
2658
|
+
# Acumulando os p&l's do periodo
|
|
2659
|
+
hist_attr = (self.get_table("hist_attr_base", index=True, index_name="Key")
|
|
2660
|
+
.query("Fund == @fund and Start_Date>= @previous_date and End_Date <= @recent_date")
|
|
2661
|
+
)
|
|
2662
|
+
# Corrigindo datas de inicio e fim, para datas validas considerando as janelas de attribution calculadas.
|
|
2663
|
+
previous_date = hist_attr["Start_Date"].min().date()
|
|
2664
|
+
recent_date = hist_attr["End_Date"].max().date()
|
|
2665
|
+
|
|
2666
|
+
hist_attr = hist_attr[["p&l_brl","p&l_brl_curncy","p&l_usd","p&l_usd_curncy", "attr_brl", "attr_brl_curncy", "attr_usd", "attr_usd_curncy"]].groupby("Key").sum()
|
|
2667
|
+
|
|
2668
|
+
#hist_attr = hist_attr[["p&l_brl","p&l_brl_curncy","p&l_usd","p&l_usd_curncy"]].groupby("Key").sum()
|
|
2669
|
+
|
|
2670
|
+
|
|
2671
|
+
# Vamos encontrar o total de p&l do periodo e a rentabilidade no periodo, em reais e em dolares
|
|
2672
|
+
end_usdbrl = self.get_price("bmfxclco curncy", recent_date)
|
|
2673
|
+
average_usdbrl = self.get_table("hist_px_last").query("Asset == 'usdbrl curncy' and Date >= @previous_date and Date <= @recent_date")["Last_Price"].mean()
|
|
2674
|
+
usdbrl_variation = self.get_pct_change("bmfxclco curncy", previous_date=previous_date, recent_date=recent_date)
|
|
2675
|
+
|
|
2676
|
+
# Calculando pnl e rentab totais (ainda nao sabemos em qual moeda esta)
|
|
2677
|
+
total_pnl = self.get_fund_pnl(fund, previous_date, recent_date)
|
|
2678
|
+
total_rentab = self.get_pct_change(fund, previous_date=previous_date, recent_date=recent_date)
|
|
2679
|
+
others_pnl = total_pnl - hist_attr["p&l_"+fund_currency].sum()
|
|
2680
|
+
others_pnl_curncy = total_pnl - hist_attr["p&l_"+fund_currency+"_curncy"].sum()
|
|
2681
|
+
|
|
2682
|
+
# Inicializando todas as possibilidades
|
|
2683
|
+
total_pnl_brl, total_pnl_usd, total_rentab_brl, total_rentab_usd = total_pnl, total_pnl, total_rentab, total_rentab
|
|
2684
|
+
others_pnl_brl, others_pnl_usd = others_pnl, others_pnl
|
|
2685
|
+
others_pnl_brl_curncy, others_pnl_usd_curncy = others_pnl_curncy, others_pnl_curncy
|
|
2686
|
+
|
|
2687
|
+
# Encontrando moeda e corrigindo valores.
|
|
2688
|
+
if fund_currency == "usd":
|
|
2689
|
+
others_pnl_brl = others_pnl_brl * end_usdbrl
|
|
2690
|
+
others_pnl_brl_curncy = others_pnl_brl_curncy * end_usdbrl
|
|
2691
|
+
total_pnl_brl = hist_attr["p&l_brl"].sum() + others_pnl_brl
|
|
2692
|
+
total_rentab_brl = (1+total_rentab_usd)*(1+usdbrl_variation)-1
|
|
2693
|
+
# Nesse caso:
|
|
2694
|
+
# O pnl total precisa ter o mesmo sinal da rentabilidade total, senao algo esta errado.
|
|
2695
|
+
if total_pnl_brl * total_rentab_brl < 0:
|
|
2696
|
+
print(f"Rentabilidade e pnl com sinais trocados. Conferir '{fund}' de '{previous_date}' ate '{recent_date}'.")
|
|
2697
|
+
print(f"Obtendo pnl total a partir da rentabilidade e PL medio do periodo")
|
|
2698
|
+
fund_quotas = self.get_table("all_funds_quotas").query("Fund ==@fund and Date >= @previous_date and Date <= @recent_date")
|
|
2699
|
+
total_pnl_brl = (fund_quotas["#"] * fund_quotas["Quota"]).mean() * average_usdbrl * total_rentab_brl
|
|
2700
|
+
|
|
2701
|
+
elif fund_currency == "brl":
|
|
2702
|
+
others_pnl_usd =others_pnl_usd / end_usdbrl
|
|
2703
|
+
others_pnl_usd_curncy = others_pnl_usd_curncy / end_usdbrl
|
|
2704
|
+
total_pnl_usd = hist_attr["p&l_usd"].sum() + others_pnl_usd
|
|
2705
|
+
total_rentab_usd = ((1+total_rentab_brl) / (1+usdbrl_variation))-1
|
|
2706
|
+
# Nesse caso:
|
|
2707
|
+
# O pnl total precisa ter o mesmo sinal da rentabilidade total, senao algo esta errado.
|
|
2708
|
+
if total_pnl_usd * total_rentab_usd < 0:
|
|
2709
|
+
print(f"Rentabilidade e pnl com sinais trocados. Conferir '{fund}' de '{previous_date}' ate '{recent_date}'.")
|
|
2710
|
+
print(f"Obtendo pnl total a partir da rentabilidade e PL medio do periodo")
|
|
2711
|
+
fund_quotas = self.get_table("all_funds_quotas").query("Fund ==@fund and Date >= @previous_date and Date <= @recent_date")
|
|
2712
|
+
total_pnl_usd = (fund_quotas["#"] * fund_quotas["Quota"]).mean() / average_usdbrl * total_rentab_usd
|
|
2713
|
+
|
|
2714
|
+
others_attr_brl = total_rentab_brl - hist_attr["attr_brl"].sum()
|
|
2715
|
+
others_attr_brl_curncy = total_rentab_brl - hist_attr["attr_brl_curncy"].sum()
|
|
2716
|
+
others_attr_usd = total_rentab_usd - hist_attr["attr_usd"].sum()
|
|
2717
|
+
others_attr_usd_curncy = total_rentab_usd - hist_attr["attr_usd_curncy"].sum()
|
|
2718
|
+
|
|
2719
|
+
# Vamos inserir o row 'outros' com o que falta de pnl para atingir a rentabilidade do periodo
|
|
2720
|
+
hist_attr.loc["outros_outros"] = [others_pnl_brl, others_pnl_brl_curncy, others_pnl_usd, others_pnl_usd_curncy, others_attr_brl, others_attr_brl_curncy, others_attr_usd, others_attr_usd_curncy]
|
|
2721
|
+
|
|
2722
|
+
#hist_attr.loc["outros_outros"] = [others_pnl_brl, others_pnl_brl_curncy, others_pnl_usd, others_pnl_usd_curncy]
|
|
2723
|
+
|
|
2724
|
+
|
|
2725
|
+
# Vamos calcular o % de atribuicao, proporcional a variacao da cota no periodo
|
|
2726
|
+
# Em reais
|
|
2727
|
+
#hist_attr["attr_brl"] = hist_attr.apply(lambda row: ((row["p&l_brl"])/abs(row["p&l_brl"])) * abs(row["p&l_brl"] * total_rentab_brl/total_pnl_brl), axis=1)
|
|
2728
|
+
#hist_attr["attr_brl_curncy"] = hist_attr.apply(lambda row: row["p&l_brl_curncy"] * total_rentab_brl/total_pnl_brl, axis=1)
|
|
2729
|
+
# Em dolares
|
|
2730
|
+
#hist_attr["attr_usd"] = hist_attr.apply(lambda row: row["p&l_usd"] * total_rentab_usd/total_pnl_usd, axis=1)
|
|
2731
|
+
#hist_attr["attr_usd_curncy"] = hist_attr.apply(lambda row: row["p&l_usd_curncy"] * total_rentab_usd/total_pnl_usd, axis=1)
|
|
2732
|
+
|
|
2733
|
+
|
|
2734
|
+
hist_attr["Start_Date"] = previous_date
|
|
2735
|
+
hist_attr["End_Date"] = recent_date
|
|
2736
|
+
|
|
2737
|
+
return hist_attr
|
|
2738
|
+
|
|
2739
|
+
|
|
2740
|
+
def calculate_drawdown(self, tickers, previous_date, recent_date, currency="local"):
|
|
2741
|
+
"""Calcula o drawdown para um ativo ou um conjunto de ativos em cada data do perido fornecido.
|
|
2742
|
+
|
|
2743
|
+
Args:
|
|
2744
|
+
tickers (str|list|set): ticker ou lista de tickers
|
|
2745
|
+
previous_date (dt.date): _description_
|
|
2746
|
+
recent_date (dt.date):
|
|
2747
|
+
|
|
2748
|
+
Returns:
|
|
2749
|
+
pd.DataFrame: Seguinte DataFrame usando Date como index -> [[Asset, Period_Change, Previous_Peak, Drawdowns]]
|
|
2750
|
+
"""
|
|
2751
|
+
|
|
2752
|
+
df = self.get_prices(tickers=tickers, previous_date=previous_date, recent_date=recent_date, currency=currency).set_index("Date")
|
|
2753
|
+
|
|
2754
|
+
df["Period_Change"] = df.groupby("Asset").pct_change().fillna(0) + 1
|
|
2755
|
+
df["Period_Change"] = df[["Asset", "Period_Change"]].groupby("Asset").cumprod()
|
|
2756
|
+
df["Previous_Peak"] = df[["Asset", "Period_Change"]].groupby("Asset").cummax()
|
|
2757
|
+
df["Drawdowns"] = (df["Period_Change"] - df["Previous_Peak"])/df["Previous_Peak"]
|
|
2758
|
+
|
|
2759
|
+
return df
|
|
2760
|
+
|
|
2761
|
+
|
|
2762
|
+
def calculate_tracking_error(self, returns_ticker, benchmark_ticker, previous_date=dt.date.today()-dt.timedelta(days=365*2), recent_date=dt.date.today()-dt.timedelta(days=1), period=None, holiday_location="all"):
|
|
2763
|
+
""" Calcula o tracking error de uma serie de retorno com relacao ao benchmark.
|
|
2764
|
+
|
|
2765
|
+
Args:
|
|
2766
|
+
returns_ticker (str): ticker da serie de retorno que sera testada
|
|
2767
|
+
benchmark_ticker (str): ticker da serie de retorno que sera usada para comparacao
|
|
2768
|
+
previous_date (dt.date): data de inicio do teste
|
|
2769
|
+
recent_date (dt.date): data de fim do teste
|
|
2770
|
+
Returns:
|
|
2771
|
+
_type_: _description_
|
|
2772
|
+
"""
|
|
2773
|
+
|
|
2774
|
+
if period is not None:
|
|
2775
|
+
previous_date = self.get_start_period_dt(recent_date, period=period, holiday_location=holiday_location)
|
|
2776
|
+
|
|
2777
|
+
|
|
2778
|
+
returns = self.get_prices([returns_ticker, benchmark_ticker], previous_date=previous_date, recent_date=recent_date, currency="usd")
|
|
2779
|
+
|
|
2780
|
+
returns = returns.set_index("Date").pivot(columns="Asset").dropna(how="any")
|
|
2781
|
+
returns = returns.pct_change().dropna()
|
|
2782
|
+
|
|
2783
|
+
returns.columns = [returns_ticker, benchmark_ticker]
|
|
2784
|
+
|
|
2785
|
+
n_periods = len(returns)
|
|
2786
|
+
returns["Diff"] = (returns[returns_ticker] - returns[benchmark_ticker])**2
|
|
2787
|
+
|
|
2788
|
+
tracking_error = 0
|
|
2789
|
+
if (n_periods - 1) > 0:
|
|
2790
|
+
tracking_error = (returns["Diff"].sum()/(n_periods - 1)) ** (1/2)
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
return tracking_error
|
|
2794
|
+
|
|
2795
|
+
|
|
2796
|
+
def xirr(self, cashflows, dates):
|
|
2797
|
+
"""
|
|
2798
|
+
Calcula o XIRR para uma serie de cash flows em datas especificas.
|
|
2799
|
+
|
|
2800
|
+
:cashflows (numpy.array of float): cash flows (positive para entrada, negativo para saida)
|
|
2801
|
+
:dates (numpy.array of datetime64): datas correspondentes aos cash flows
|
|
2802
|
+
:return: XIRR
|
|
2803
|
+
"""
|
|
2804
|
+
# Garantindo que dates eh um numpy array de datetime64
|
|
2805
|
+
if not isinstance(dates, np.ndarray) or dates.dtype.type is not np.datetime64:
|
|
2806
|
+
dates = np.array(pd.to_datetime(dates))
|
|
2807
|
+
|
|
2808
|
+
t0 = dates[0] # Reference date
|
|
2809
|
+
years = np.array([(date - t0).astype('timedelta64[D]').astype(int) / 365.25 for date in dates])
|
|
2810
|
+
|
|
2811
|
+
def xnpv(rate):
|
|
2812
|
+
# Limiting the rate to avoid overflow and division by zero
|
|
2813
|
+
rate = max(min(rate, 1), -0.9999)
|
|
2814
|
+
return sum([cf / (1 + rate) ** yr for cf, yr in zip(cashflows, years)])
|
|
2815
|
+
|
|
2816
|
+
def xnpv_prime(rate):
|
|
2817
|
+
rate = max(min(rate, 1), -0.9999)
|
|
2818
|
+
return sum([-yr * cf / (1 + rate) ** (yr + 1) for cf, yr in zip(cashflows, years)])
|
|
2819
|
+
|
|
2820
|
+
try:
|
|
2821
|
+
# Using a conservative initial guess
|
|
2822
|
+
initial_guess = 0.1
|
|
2823
|
+
return newton(xnpv, x0=initial_guess, fprime=xnpv_prime)
|
|
2824
|
+
except (RuntimeError, OverflowError):
|
|
2825
|
+
# Return NaN if the calculation fails
|
|
2826
|
+
return np.nan
|
|
2827
|
+
|
|
2828
|
+
|
|
2829
|
+
def calculate_price_correlation(self, assets_data:dict, sample_frequency="monthly", period="12m", ref_date=dt.date.today()):
|
|
2830
|
+
"""
|
|
2831
|
+
Calculate the correlation between assets in the columns of 'asset_prices'. The correlation is calculated using the formula:
|
|
2832
|
+
corr = (1 + r1*r2 + r1*r2*corr) / sqrt(1+r1**2) * sqrt(1+r2**2)
|
|
2833
|
+
where r1 and r2 are the returns of the assets and corr is the correlation between the returns.
|
|
2834
|
+
The correlation is calculated using the formula above and the returns are calculated using the formula:
|
|
2835
|
+
r = (price[t] - price[t-1]) / price[t-1]
|
|
2836
|
+
where t is the time period.
|
|
2837
|
+
The returns are calculated using the sample frequency and the lookback in months.
|
|
2838
|
+
assets_data: dict mapeando ticker para {Name:str, Key:str e Location:str(bz ou us)}
|
|
2839
|
+
sample_frequency: str, default "monthly". Frequencia de amostragem dos precos. Pode ser "daily", "weekly" ou "monthly"
|
|
2840
|
+
period: str, default '12m'. Periodo de calculo da correlacao. Deve ser 'ytd' ou o numero de meses seguido por 'm'
|
|
2841
|
+
"""
|
|
2842
|
+
sample_frequency = sample_frequency.lower()
|
|
2843
|
+
period = period.lower()
|
|
2844
|
+
asset_prices = self.get_prices(tickers=set(assets_data.keys()) - {"usdbrl curncy"}, recent_date=ref_date,
|
|
2845
|
+
period=period, currency="usd", usdbrl_ticker="usdbrl curncy")
|
|
2846
|
+
usdbrl_prices = self.get_prices(tickers={"usdbrl curncy"}, period=period)
|
|
2847
|
+
asset_prices = pd.concat([asset_prices, usdbrl_prices])
|
|
2848
|
+
|
|
2849
|
+
asset_prices = asset_prices.pivot(index="Date", columns="Asset", values="Last_Price").ffill()
|
|
2850
|
+
|
|
2851
|
+
|
|
2852
|
+
frequencies = {"daily":"B", "weekly":"W", "monthly":"ME"}
|
|
2853
|
+
asset_prices = asset_prices.resample(frequencies[sample_frequency]).last()
|
|
2854
|
+
#returns = asset_prices.pct_change(lookback_in_months)
|
|
2855
|
+
#returns = returns.dropna()
|
|
2856
|
+
correlations = asset_prices.corr()
|
|
2857
|
+
correlations.index.name = "Asset_Corr"
|
|
2858
|
+
correlations = correlations.reset_index()
|
|
2859
|
+
|
|
2860
|
+
value_vars = correlations.columns
|
|
2861
|
+
correlations = correlations.melt(id_vars=["Asset_Corr"], value_vars=value_vars, value_name="Correlation", var_name="Asset").sort_values(by="Correlation").reset_index(drop=True)
|
|
2862
|
+
correlations["Key_Asset_Corr"] = correlations["Asset_Corr"].apply(lambda x: assets_data[x]["Key"])
|
|
2863
|
+
correlations["Key_Asset"] = correlations["Asset"].apply(lambda x: assets_data[x]["Key"])
|
|
2864
|
+
correlations.index.name="Order"
|
|
2865
|
+
correlations = correlations.reset_index()
|
|
2866
|
+
correlations["Comp_Key"] = correlations["Key_Asset_Corr"]+"_"+correlations["Key_Asset"]
|
|
2867
|
+
correlations["Frequency"] = sample_frequency.replace("ly","").title()
|
|
2868
|
+
period = period if int(period.replace("m","").replace("ytd","0")) < 12 else str(int(period.replace("m",""))/12).replace(".0","")+"y"
|
|
2869
|
+
correlations["Period"] = period.upper()
|
|
2870
|
+
|
|
2871
|
+
return correlations
|
|
2872
|
+
|
|
2873
|
+
|
|
2874
|
+
def calculate_volatility(self, tickers, min_rolling_period_months, analysis_period_days, currency="local", sample_frequency="monthly", usdbrl_ticker="usdbrl curncy"):
|
|
2875
|
+
|
|
2876
|
+
period = min_rolling_period_months + analysis_period_days//60 + 3
|
|
2877
|
+
prices = self.get_prices(tickers=tickers, period=str(period)+"m", currency=currency, usdbrl_ticker=usdbrl_ticker)
|
|
2878
|
+
|
|
2879
|
+
year_size = 260
|
|
2880
|
+
|
|
2881
|
+
if sample_frequency == "monthly":
|
|
2882
|
+
analysis_period_days = analysis_period_days//30 # numero de meses para rolling
|
|
2883
|
+
prices = prices.groupby("Asset").resample("ME", on="Date").last().reset_index(level=1).reset_index(drop=True).set_index("Date")
|
|
2884
|
+
year_size = 12
|
|
2885
|
+
|
|
2886
|
+
elif sample_frequency == 'weekly':
|
|
2887
|
+
analysis_period_days = analysis_period_days//7 # numero de semanas para rolling
|
|
2888
|
+
prices = prices.groupby("Asset").resample("W", on="Date").last().reset_index(level=1).reset_index(drop=True).set_index("Date")
|
|
2889
|
+
year_size = 52
|
|
2890
|
+
|
|
2891
|
+
elif sample_frequency == "daily":
|
|
2892
|
+
prices["Yesterday"] = prices["Date"] - dt.timedelta(days=1)
|
|
2893
|
+
prices["Equals_To_Yesterday"] = (prices["Last_Price"] == prices["Last_Price"].shift(1)) & (prices["Yesterday"] == prices["Date"].shift(1))
|
|
2894
|
+
prices = prices.query("~Equals_To_Yesterday")[["Date", "Asset", "Last_Price"]].copy().set_index("Date")
|
|
2895
|
+
|
|
2896
|
+
|
|
2897
|
+
prices["Previous_Price"] = prices.groupby("Asset")["Last_Price"].shift(1)
|
|
2898
|
+
prices["Return"] =np.log((prices["Last_Price"]/prices["Previous_Price"])).fillna(0)
|
|
2899
|
+
|
|
2900
|
+
prices = prices[["Asset", "Return"]].groupby("Asset").rolling(window=analysis_period_days).std()*np.sqrt(year_size)*100
|
|
2901
|
+
|
|
2902
|
+
prices = prices.reset_index().dropna().rename(columns={"Asset":"Ticker", "Return":"Volatility"})
|
|
2903
|
+
|
|
2904
|
+
return prices
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
def get_asset_name(self, ticker):
|
|
2908
|
+
|
|
2909
|
+
assets = self.get_table("assets")
|
|
2910
|
+
name = assets.query("Ticker == @ticker")["Name"].squeeze()
|
|
2911
|
+
return name
|
|
2912
|
+
|
|
2913
|
+
|
|
2914
|
+
def list_blob_files(self, sub_dir, ends_with=None):
|
|
2915
|
+
"""
|
|
2916
|
+
Lista todos os arquivos dentro de um diretorio no blob storage.
|
|
2917
|
+
"""
|
|
2918
|
+
|
|
2919
|
+
# Vamos listar os arquivos do diretorio
|
|
2920
|
+
blob_files = self.container_client.list_blobs(name_starts_with=sub_dir)
|
|
2921
|
+
|
|
2922
|
+
if ends_with is not None:
|
|
2923
|
+
return [blob_file.name for blob_file in blob_files if blob_file.name.endswith(ends_with)]
|
|
2924
|
+
return [blob_file.name for blob_file in blob_files]
|
|
2925
|
+
|
|
2926
|
+
|
|
2927
|
+
def simulate_portfolio_performance(self, portfolio: dict, portfolio_date: dt.date, adm_fee: float, performance_fee: float = 0):
|
|
2928
|
+
"""
|
|
2929
|
+
Simula o desempenho de um portfólio com base em um dicionário de ativos e suas alocações iniciais.
|
|
2930
|
+
|
|
2931
|
+
Parâmetros:
|
|
2932
|
+
- portfolio: dicionário com os ativos e suas alocações iniciais. Dicionário deve seguir o formato:\n
|
|
2933
|
+
{
|
|
2934
|
+
"ticker1": peso1,
|
|
2935
|
+
"ticker2": peso2,
|
|
2936
|
+
...
|
|
2937
|
+
"tickerN": pesoN
|
|
2938
|
+
}, onde os pesos devem somar 1.
|
|
2939
|
+
- portfolio_date: data inicial do portfólio.
|
|
2940
|
+
- adm_fee: taxa de administração %.a.a
|
|
2941
|
+
- performance_fee: taxa de performance (opcional).
|
|
2942
|
+
|
|
2943
|
+
Retorna:
|
|
2944
|
+
- DataFrame com o fator de correção da cota para cada dia a partir da data inicial do portfolio.
|
|
2945
|
+
"""
|
|
2946
|
+
|
|
2947
|
+
# Formatar os tickers para minusculas
|
|
2948
|
+
portfolio = {k.lower(): v for k, v in portfolio.items()}
|
|
2949
|
+
|
|
2950
|
+
initial_portfolio = {
|
|
2951
|
+
"date" : portfolio_date,
|
|
2952
|
+
"assets" : portfolio,
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
# Criando dataframe com as colunas Date|Ticker|Weight|
|
|
2956
|
+
positions = pd.DataFrame(initial_portfolio["assets"].items(), columns=["Ticker", "Weight"])
|
|
2957
|
+
positions["Date"] = initial_portfolio["date"]
|
|
2958
|
+
positions["Returns"] = 0
|
|
2959
|
+
positions["Daily_Attribution"] = 0.0
|
|
2960
|
+
positions["Daily_Portfolio_Return"] = 0.0
|
|
2961
|
+
|
|
2962
|
+
tickers = positions["Ticker"].tolist()
|
|
2963
|
+
|
|
2964
|
+
max_date = portfolio_date
|
|
2965
|
+
|
|
2966
|
+
while max_date < dt.date.today():
|
|
2967
|
+
# Adicionando 1 dia
|
|
2968
|
+
new_date = max_date + dt.timedelta(days=1)
|
|
2969
|
+
daily_returns = self.get_pct_changes(tickers=tickers, previous_date=max_date, recent_date=new_date, currency="usd")
|
|
2970
|
+
|
|
2971
|
+
daily_returns.index.name = "Ticker"
|
|
2972
|
+
|
|
2973
|
+
new_day = positions.query("Date == @max_date").copy().set_index("Ticker")
|
|
2974
|
+
new_day["Returns"] = daily_returns
|
|
2975
|
+
new_day = new_day.reset_index()
|
|
2976
|
+
# Ajusta pesos considerando a atribuicao de retorno do dia anterior
|
|
2977
|
+
new_day["Weight"] = new_day["Weight"] + new_day["Daily_Attribution"]
|
|
2978
|
+
new_day["Date"] = new_date
|
|
2979
|
+
|
|
2980
|
+
new_day["Daily_Attribution"] = new_day["Weight"] * new_day["Returns"]
|
|
2981
|
+
new_day["Daily_Portfolio_Return"] = new_day["Daily_Attribution"].sum()
|
|
2982
|
+
|
|
2983
|
+
positions = pd.concat([positions, new_day])
|
|
2984
|
+
max_date = new_date
|
|
2985
|
+
|
|
2986
|
+
positions["Date"] = pd.to_datetime(positions["Date"])
|
|
2987
|
+
daily_returns = positions[["Date", "Daily_Portfolio_Return"]].groupby("Date").last()
|
|
2988
|
+
|
|
2989
|
+
daily_returns["Acc_Returns"] = (1 + daily_returns["Daily_Portfolio_Return"]).cumprod()
|
|
2990
|
+
daily_returns["Acc_Returns_Adjusted_by_Taxes"] = np.where(daily_returns["Acc_Returns"] > 1,
|
|
2991
|
+
((daily_returns["Acc_Returns"] -1 ) * (1-performance_fee) + 1) - adm_fee/12,
|
|
2992
|
+
daily_returns["Acc_Returns"] - adm_fee/12
|
|
2993
|
+
)
|
|
2994
|
+
|
|
2995
|
+
return daily_returns.reset_index()[["Date", "Acc_Returns_Adjusted_by_Taxes"]].rename(columns={"Acc_Returns_Adjusted_by_Taxes": "Factor"})
|
|
2996
|
+
|