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.
@@ -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
+