aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--python/monthend_interest_recon.py316
1 files changed, 193 insertions, 123 deletions
diff --git a/python/monthend_interest_recon.py b/python/monthend_interest_recon.py
index 43fc8d82..f1cb4305 100644
--- a/python/monthend_interest_recon.py
+++ b/python/monthend_interest_recon.py
@@ -1,13 +1,16 @@
import datetime
import logging
import argparse
-from pathlib import Path
from collections import defaultdict
+from io import StringIO
+import re
+
import pandas as pd
from dateutil.relativedelta import relativedelta
+from dataclasses import dataclass
from serenitas.utils.env import DAILY_DIR
-from serenitas.utils.exchange import ExchangeMessage
+from serenitas.utils.exchange import ExchangeMessage, FileAttachment
from collateral.common import load_pdf
from interest_statement import export_data
@@ -17,170 +20,237 @@ from report_ops.misc import em_date_filter
logger = logging.getLogger(__name__)
-def get_fpath(counterparty, save=False):
- if save:
- return (
- DAILY_DIR / "Serenitas" / f"{counterparty}_reports" / "Interest Statements"
- )
- else:
- return DAILY_DIR / "Serenitas" / "MonthlyInterest" / f"{counterparty}_reports"
+def get_pat(month, year, counterparty):
+ match counterparty:
+ case "BoA":
+ month_abbr = datetime.datetime.strptime(str(month), "%m").strftime("%b")
+ pattern = f"??{month_abbr}{year}*.pdf"
+ case "BNP":
+ month_name = datetime.datetime.strptime(str(month), "%m").strftime("%B")
+ pattern = (
+ f"Interest Statement SCAR GCM_RVM - USD - ?? {month_name} {year}.pdf"
+ )
+ case "CITI":
+ pattern = f"*_USD_ICS_SYSTEM.*.{year}{str(month).zfill(2)}??.xlsx"
+ case "JPM":
+ pattern = f"CSINSTMT-*-{str(year)[2:]}{str(month).zfill(2)}??-*_*_*.pdf"
+ case "GS":
+ pattern = f"MVPInterestStatement_{year}{str(month).zfill(2)}??_*_*.pdf"
+ case "MS":
+ pattern = f"*IntStmt_{year}{str(month).zfill(2)}??.xls"
+ case _:
+ raise ValueError(f"Missing counterparty: {counterparty}")
+ return pattern
-def download_messages(em, counterparty, start, end, save=False):
- for msg in em.get_msgs(
- 20, path=["Interest", counterparty], **em_date_filter(em, start, end)
- ):
- base_dir = get_fpath(counterparty, save)
- for attach in msg.attachments:
- fname = attach.name
- if (counterparty == "CS") and not ("Interest" in fname):
- continue
- p = base_dir / fname
- if not p.parent.exists():
- p.parent.mkdir(parents=True)
- if not p.exists():
- p.write_bytes(attach.content)
+def download_messages(em, counterparties, start, end):
+ for counterparty in counterparties:
+ for msg in em.get_msgs(
+ 20, path=["Interest", counterparty], **em_date_filter(em, start, end)
+ ):
+ BASE_DIR = (
+ DAILY_DIR
+ / "Serenitas"
+ / f"{counterparty}_reports"
+ / "Interest Statements"
+ )
+ for attach in msg.attachments:
+ p = BASE_DIR / attach.name
+ p.parent.mkdir(parents=True, exist_ok=True)
+ if not p.exists():
+ p.write_bytes(attach.content)
-def get_CS(g):
- for e in g[-1]:
- if "This interest, margin" in e.text:
- if value == "No Accruals to Report":
- return 0
- return float(value)
- value = e.text
+@dataclass
+class InterestCounterparty:
+ month: datetime.date.month
+ year: datetime.date.year
+ _registry = {}
+ def __init_subclass__(cls, name):
+ cls.name = name
+ cls._registry[name] = cls
-def get_BNP(l):
- for e, n in zip(l, l[1:]):
- if "Due to" in e.text:
- value = n.text.replace(",", "")
- return -float(value)
+ def __class_getitem__(cls, name: str):
+ return cls._registry[name]
+ def yield_files(self):
+ BASE_DIR = (
+ DAILY_DIR / "Serenitas" / f"{self.name}_reports" / "Interest Statements"
+ )
+ yield from BASE_DIR.glob(self.pat)
-def get_CITI(path):
- df = pd.read_excel(path)
- for row in df.itertuples():
- if "Net Interest Due To CP" in row or "Net Interest Due to Citi" in row:
- return -row._6
+ @property
+ def pat(self):
+ statement_month = datetime.date(
+ year=self.year, month=self.month, day=1
+ ) + relativedelta(months=1)
+ return get_pat(statement_month.month, statement_month.year, self.name)
-def get_GS(l):
- for e, n in zip(l, l[1:]):
- if "due to" in e.text:
- return float(n.text.replace("USD", "").replace(",", ""))
+class BoA(InterestCounterparty, name="BoA"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ pdf = load_pdf(file_path)
+ for e, n in zip(pdf, pdf[1:]):
+ if "Net interest Amount" in e.text:
+ return -float(
+ n.text.replace("(", "-").replace(")", "").replace(",", "")
+ )
-def get_MS(path):
- df = pd.read_excel(path)
- return -round(df["LOCAL_ACCRUAL"].sum(), 2)
+class BNP(InterestCounterparty, name="BNP"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ pdf = load_pdf(file_path)
+ for e, n in zip(pdf, pdf[1:]):
+ if "Due to" in e.text:
+ value = n.text.replace(",", "")
+ return -float(value)
+ @property
+ def pat(self):
+ # BNP files reflect month of interest rather than month of statement
+ return get_pat(self.month, self.year, self.name)
-def get_BAML(l):
- for e, n in zip(l, l[1:]):
- if "Net interest Amount" in e.text:
- return -float(n.text.replace("(", "-").replace(")", "").replace(",", ""))
+class CITI(InterestCounterparty, name="CITI"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ df = pd.read_excel(file_path, skiprows=14)
+ for row in df.itertuples():
+ if "Net Interest Due To CP" in row or "Net Interest Due to Citi" in row:
+ return -row._6
-def get_JPM(g):
- for e in g:
- if "Page" in e.text:
- return float(value.replace(",", ""))
- value = e.text
+class JPM(InterestCounterparty, name="JPM"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ pdf = load_pdf(file_path)
+ for e in pdf:
+ if "Page" in e.text:
+ return float(value.replace(",", ""))
+ value = e.text
-def start_end(date):
- date = date - relativedelta(months=1)
- start = datetime.date(date.year, date.month, 1)
- end = start + relativedelta(months=1)
- end -= datetime.timedelta(days=1)
- return start, end
+ @property
+ def pat(self):
+ # JPM files reflect month of interest rather than month of statement
+ return get_pat(self.month, self.year, self.name)
-def get_interest(counterparties, save=False):
- interest_amounts = defaultdict(float)
- for cp in counterparties:
- try:
- func = globals()[f"get_{cp}"]
- except KeyError:
- print(f"Missing cp {cp}")
- base_path = Path("/home/serenitas/Daily/Serenitas/MonthlyInterest/")
- if cp in ("CITI", "MS"):
- for p in (base_path / f"{cp}_reports").glob("*.xls*"):
- if p.name.startswith("~"):
- continue
- amount = func(p)
- interest_amounts[cp] = interest_amounts[cp] + amount
- else:
- for p in (base_path / f"{cp}_reports").glob("*.pdf"):
- amount = func(load_pdf(p, pages=True if cp == "CS" else False))
- interest_amounts[cp] = interest_amounts[cp] + amount
- return pd.DataFrame(interest_amounts, index=[0]).T.rename(
- index={"BAML": "BAML_ISDA"}, columns={0: "monthly_statement"}
- )
+class GS(InterestCounterparty, name="GS"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ pdf = load_pdf(file_path)
+ for e, n in zip(pdf, pdf[1:]):
+ if "due to" in e.text:
+ return float(n.text.replace("USD", "").replace(",", ""))
+ @property
+ def pat(self):
+ # GS files reflect month of interest rather than month of statement
+ return get_pat(self.month, self.year, self.name)
-if __name__ == "__main__":
- em = ExchangeMessage()
- counterparties = ["BNP", "CITI", "GS", "MS", "BAML", "JPM"]
- # First run --save, then run --accept if it looks good
- parser = argparse.ArgumentParser(description="Save, accept within tolerances")
+
+class MS(InterestCounterparty, name="MS"):
+ @staticmethod
+ def get_interest_amount(file_path):
+ df = pd.read_excel(file_path)
+ return -round(df["LOCAL_ACCRUAL"].sum(), 2)
+
+ @property
+ def pat(self):
+ # GS files reflect month of interest rather than month of statement
+ return get_pat(self.month, self.year, self.name)
+
+
+# class CS(InterestCounterparty, name='CS'):
+# @staticmethod
+# def get_interest_amount(file_path):
+# pdf = load_pdf(file_path, pages=True)
+# for row in pdf[-1]:
+# if "This interest, margin" in row.text:
+# if row == "No Accruals to Report":
+# return 0
+# return float(row)
+
+
+def parse_args():
+ """Parses command line arguments"""
+ parser = argparse.ArgumentParser(description="Generate IAM file for globeop")
parser.add_argument(
- "date",
- type=datetime.date.fromisoformat,
+ "monthend",
nargs="?",
- default=datetime.date.today(),
- )
- parser.add_argument(
- "--save",
- action="store_true",
- default=False,
- help="store in proper folders",
+ type=datetime.date.fromisoformat,
+ default=datetime.date.today().replace(day=1) - datetime.timedelta(days=1),
)
parser.add_argument(
"--accept",
+ "-a",
action="store_true",
default=False,
help="accept the differences are within tolerance and edit the csv accordingly",
)
- args = parser.parse_args()
- start = datetime.date(args.date.year, args.date.month, 1)
+ return parser.parse_args()
- for cp in counterparties:
- download_messages(em, cp, start, args.date, args.save)
- interest_amounts = get_interest(counterparties, save=args.save)
- start, end = start_end(args.date)
+def get_statement_totals(counterparties, month, year):
+ interest_amounts = defaultdict(float)
+ for counterparty in counterparties:
+ interest_counterparty = InterestCounterparty[counterparty](month, year)
+ for f in interest_counterparty.yield_files():
+ interest_amounts[counterparty] += interest_counterparty.get_interest_amount(
+ f
+ )
+ return pd.DataFrame.from_dict(
+ interest_amounts, orient="index", columns=["statement_interest"]
+ ).rename(index={"BoA": "BAML_ISDA"})
+
+
+def main():
+ args = parse_args()
+ em = ExchangeMessage()
+ download_messages(
+ em,
+ InterestCounterparty._registry.keys(),
+ args.monthend.replace(day=1),
+ args.monthend + relativedelta(months=1),
+ )
global df
+ df = get_statement_totals(
+ InterestCounterparty._registry.keys(), args.monthend.month, args.monthend.year
+ )
+ serenitas_calculations = export_data(
+ args.monthend - relativedelta(months=1) + datetime.timedelta(days=1),
+ args.monthend,
+ )
df = pd.merge(
- export_data(start, end).groupby("broker").sum(),
- interest_amounts,
+ serenitas_calculations.groupby("broker").sum(),
+ df,
how="outer",
left_index=True,
right_index=True,
)
df = df.fillna(0)
- df["difference"] = df["amount"] - df["monthly_statement"]
+ df["difference"] = df["amount"] - df["statement_interest"]
if args.accept:
- ssnc_df = export_data(start, end)
- for k, v in df["difference"].items():
- ssnc_df.loc[k].loc["CSH_CASH"] -= v
- from io import StringIO
- from serenitas.utils.exchange import ExchangeMessage, FileAttachment
-
+ for cp, difference in df["difference"].items():
+ # Match with counterparty if within tolerance
+ serenitas_calculations.loc[cp].loc["CSH_CASH"] -= difference
buf = StringIO()
- ssnc_df.to_csv(buf)
- attach = [
- FileAttachment(
- name=f"{start:%b}.csv",
- content=buf.getvalue().encode(),
- )
- ]
- em = ExchangeMessage()
+ serenitas_calculations.to_csv(buf)
em.send_email(
- subject=f"Allocation of Interest for {start:%b}",
- body="Please see attached",
+ subject=f"Allocation of Interest for {args.monthend.month:%b}",
+ body="Please see attached for allocation of interest by strategy and counterparty",
to_recipients=("serenitas.otc@sscinc.com", "SERENITAS.ops@sscinc.com"),
cc_recipients=("nyops@lmcg.com",),
- attach=attach,
+ attach=[
+ FileAttachment(
+ name=f"{args.monthend:%b-%y}.csv", content=buf.getvalue().encode()
+ )
+ ],
)
+
+
+if __name__ == "__main__":
+ main()