from serenitas.utils.exchange import ExchangeMessage, FileAttachment from serenitas.utils.env import DAILY_DIR import warnings import datetime from .misc import get_dir, _recipients, _cc_recipients import gpg from serenitas.ops.trade_dataclasses import BondDeal from serenitas.ops.funds import Service from typing import ClassVar from dataclasses import dataclass from psycopg2.errors import UniqueViolation _bond_query = "SELECT * FROM bond_trades WHERE trade_date=%s AND account=%s;" _csv_query = "INSERT INTO bond_csv_upload (allocationid, identifier, principal_payment, accrued_payment) VALUES (%s, %s, %s, %s)" def gen_trades(trade_details, old_trade): yield (BondDeal.from_dict(**trade_details, scaled=True), "NEW") if old_trade: trade_details.update(old_trade) yield (BondDeal.from_dict(**trade_details, scaled=True), "CANCEL") def gen_csv(trade_details, account, old_trade=None): for (trade, action) in gen_trades(trade_details, old_trade): match account: case "BBH": yield trade.to_bbh(action) case "UMB": yield trade.to_umb(action) def upload_to_custodian(account, trade_date, conn, upload, em): with conn.cursor() as c, conn.cursor() as d: c.execute( _bond_query, ( trade_date, "BAC" if account == "UMB" else account, ), ) for row in c: _service = {"BBH": "BRINKER", "UMB": "UMB"} custodian = Service[_service[account]] d.execute( "SELECT identifier, principal_payment, accrued_payment FROM bond_csv_upload WHERE allocationid=%s", (row.id,), ) if old_trade := d.fetchone(): if (row.identifier, row.principal_payment, row.accrued_payment) != ( old_trade.identifier, old_trade.principal_payment, old_trade.accrued_payment, ): d.execute( "UPDATE bond_csv_upload SET identifier=%s AND principal_payment=%s AND accrued_payment=%s WHERE allocationid=%s", ( row.identifier, row.principal_payment, row.accrued_payment, row.id, ), ) for trade in gen_csv(row._asdict(), account, old_trade._asdict()): custodian.staging_queue.append(trade) else: for trade in gen_csv(row._asdict(), account): custodian.staging_queue.append(trade) if not custodian.staging_queue: return buf, dest = custodian.build_buffer("bond") custodian.staging_queue.clear() conn.commit() if upload: custodian.upload(buf, dest.name, confirm=account != "UMB") em.send_email( f"{account}: Bond Positions Uploaded for {trade_date}", "Hi, \nWe've just uploaded the positions via SFTP. File receipt attached to this email", _recipients.get(account, _cc_recipients[custodian.name]), cc_recipients=_cc_recipients[custodian.name], reply_to=_cc_recipients[custodian.name], attach=(FileAttachment(name=dest.name, content=buf),), ) @dataclass class Custodian: date: datetime.date account: ClassVar[str] def __init_subclass__(cls, account): cls.account = account class NT(Custodian, account="NT"): @classmethod def download_reports(cls, date=datetime.date.today()): em = ExchangeMessage() for msg in em.get_msgs(path=["SeleneOps", "Passport"]): for attach in msg.attachments: message_time = attach.last_modified_time if attach.name == "Attachment1.pgp" and message_time.date() == date: dest = get_dir(message_time.date(), archived=False) dest.mkdir(exist_ok=True, parents=True) with attach.fp as fp: plaintext, result, verify_result = gpg.Context().decrypt( fp.read(), passphrase="Serenitas1" ) fname = ( "custodian_wires" if "custodian" in verify_result.file_name else "cash" ) dest = dest / f"{fname}_{message_time:%Y%m%d%H%M}.csv" with open(dest, "w") as csvFile: text = plaintext.decode("utf-8").replace("\t", ",") csvFile.write(text) class UMB(Custodian, account="UMB"): @classmethod def download_reports(cls, date=datetime.date.today()): em = ExchangeMessage() for msg in em.get_msgs(count=20, path=["NYops", "Powerstation"]): for attach in msg.attachments: timestamp = attach.last_modified_time if ( attach.name.startswith("cash_balances_umb") and timestamp.date() == date ): dest = get_dir(timestamp.date(), archived=False) dest.mkdir(exist_ok=True, parents=True) p = dest / f"umb_{timestamp:%Y%m%d%H%M}.xlsx" if not p.exists(): p.write_bytes(attach.content) class BNY(Custodian, account="BONY2"): @classmethod def download_reports(cls, date=datetime.date.today()): em = ExchangeMessage() bowdst_wire_recent = True for msg in em.get_msgs( 20, path=["BowdoinOps", "Reports"], subject__startswith="Document(s) from Reporting", ): if msg.sender == "notify@bnymellon.com": for attach in msg.attachments: fname = attach.name if fname.endswith("csv"): base_name = fname.removesuffix(".csv") file_type, date_part = base_name.split("_") match file_type: case ( "Asset Detail" | "Net Investment Earned Income by Security" | "Settled Cash Statement" ): date = datetime.datetime.strptime( date_part, "%d %b %Y" ).date() case "BowdstWires": try: date = datetime.datetime.strptime( date_part, "%Y%m%d%H%M%S" ).date() except ValueError: date = datetime.datetime.strptime( date_part, "%d %b %Y%H%M%S" ).date() case "Live-cash": date = datetime.datetime.strptime( date_part, "%Y%m%d%H%M%S" ).date() case _: warnings.warn(f"Unknown report type {file_type}") continue p = DAILY_DIR / str(date) / "Reports" / fname if not p.parent.exists(): p.parent.mkdir(parents=True, exist_ok=True) if not p.exists(): p.write_bytes(attach.content)