import datetime import math import logging from dataclasses import dataclass from typing import ClassVar from exchangelib import HTMLBody from tabulate import tabulate from serenitas.utils.exchange import ExchangeMessage, FileAttachment from serenitas.analytics.dates import next_business_day from .misc import ( _recipients, _cc_recipients, _settlement_recipients, _valuation_recipients, ) logger = logging.getLogger(__name__) def next_business_days(date, offset): for i in range(offset + 1): date = next_business_day(date) return date def round_up(n, decimals=0): multiplier = 10**decimals return math.ceil(n * multiplier) / multiplier def notify_payment_settlements(date, fund, conn): end_date = next_business_days(date, 2) with conn.cursor() as c: c.execute( "SELECT * from payment_settlements WHERE settle_date BETWEEN %s AND %s AND fund= %s AND asset_class in ('SPOT', 'SWAPTION', 'TRANCHE') ORDER BY settle_date asc", (date, end_date, fund), ) for row in c: d = row._asdict() d["settlement_amount"] = d["payment_amount"] PaymentMonitor.stage(d) PaymentMonitor.email(fund) PaymentMonitor._staging_queue.clear() def notify_fx_hedge(date, fund, conn): with conn.cursor() as c: c.execute( "SELECT * from fcm_moneyline LEFT JOIN accounts2 ON account=cash_account WHERE date=%s AND currency='EUR' AND fund=%s AND abs(current_excess_deficit) > 1000000", (date, fund), ) for row in c: d = row._asdict() d["amount"] = d["current_excess_deficit"] d["category"] = "FCM" FxHedge.stage(d) FxHedge.email(fund) FxHedge._staging_queue.clear() def check_cleared_cds(date, fund, conn): _tolerance = {"IG": 0.10, "HY": 0.20, "EU": 0.20, "XO": 0.30} with conn.cursor() as c: c.execute( "SELECT * FROM list_cds_marks(%s, NULL, %s), fx WHERE date=%s AND abs((notional*factor) - globeop_notional) < 100;", (date, fund, date), ) for row in c: d = row._asdict() d["serenitas_quote"] = d["price"] match d["index"]: case "XO" | "EU": d["admin_quote"] = 100 - ( ((d["globeop_nav"] - d["accrued"]) / d["eurusd"]) / (d["globeop_notional"] / 100) ) case _: d["admin_quote"] = 100 - ( (d["globeop_nav"] - d["accrued"]) / (d["globeop_notional"] / 100) ) d["difference"] = abs(d["price"] - d["admin_quote"]) if d["difference"] > _tolerance[d["index"]]: CDXQuoteMonitor.stage(d) CDXQuoteMonitor.email(fund) CDXQuoteMonitor._staging_queue.clear() @dataclass class Monitor: date: datetime.date headers: ClassVar = () num_format: ClassVar = [] _staging_queue: ClassVar[list] = [] _em: ClassVar = ExchangeMessage() def __init_subclass__(cls, headers, num_format=[]): cls.headers = headers cls.num_format = num_format @classmethod def stage(cls, d: dict): cls._staging_queue.append(list(d[key] for key in cls.headers)) @classmethod def format(cls): for line in cls._staging_queue: for f, i in cls.num_format: line[i] = f.format(line[i]) @classmethod def to_tabulate(cls): cls.format() t = tabulate( cls._staging_queue, headers=cls.headers, tablefmt="unsafehtml", ) return t @classmethod def clear(cls): cls._staging_queue.clear() class GFSMonitor( Monitor, headers=( "date", "portfolio", "amount", "currency", ), num_format=[("{0:,.2f}", 2)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"GFS Helper Strategy Issue: {fund}", HTMLBody( f""" Good morning,

Could you please help us with the below transfer breaks:

{cls.to_tabulate()} """ ), to_recipients=_recipients[fund], cc_recipients=_cc_recipients[fund], ) class StratMonitor( Monitor, headers=( "periodenddate", "gfstranid1", "invdesc", "knowledgedate", "periodenddate", "port", "strat", ), num_format=[], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"Invalid Strategy Issue: {fund}", HTMLBody( f""" Good morning,

The below strategies should not exist. Could you please fix this?

{cls.to_tabulate()} """ ), to_recipients=_recipients[fund], cc_recipients=_cc_recipients[fund], ) class BondMarkMonitor( Monitor, headers=( "periodenddate", "invid", "geneva_identifier", "pricelist", "knowledgedate", ), num_format=[], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"Incorrectly marked trades: {fund}", HTMLBody( f""" Good morning,

Could you please use Manager marks for the below trades going forward per Valuation Policy?:

{cls.to_tabulate()} """ ), to_recipients=_valuation_recipients[fund], cc_recipients=_cc_recipients[fund], ) class CDXQuoteMonitor( Monitor, headers=( "security_desc", "security_id", "maturity", "admin_quote", "serenitas_quote", "difference", ), num_format=[("{0:,.2f}", 3), ("{0:,.2f}", 4), ("{0:,.2f}", 5)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"CDX Quote Outside of our Tolerance: {fund}", HTMLBody( f""" Good morning,

Could you please help us with the below cleared CDX quotes outside of our tolerance:

{cls.to_tabulate()} """ ), to_recipients=_valuation_recipients[fund], cc_recipients=_cc_recipients[fund], ) class CDXNotionalMonitor( Monitor, headers=( "security_desc", "security_id", "maturity", "admin_notional", "serenitas_notional", "difference", ), num_format=[("{0:,.2f}", 3), ("{0:,.2f}", 4), ("{0:,.2f}", 5)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"CDX Notional Mismatches: {fund}", HTMLBody( f""" Good morning,

Mismatched cleared cds notional mismatches below:

{cls.to_tabulate()} """ ), to_recipients=_cc_recipients[fund], ) class SettlementMonitor( Monitor, headers=("date", "account", "currency", "projected_balance"), num_format=[("{0:,.2f}", 3)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"*ACTION REQUESTED* Projected Overdraft: {fund}", HTMLBody( f""" Hello,

We see a projected overdraft on the below dates. Please move to cover:

{cls.to_tabulate()} """ ), to_recipients=_recipients[fund], cc_recipients=_cc_recipients[fund], ) class PaymentMonitor( Monitor, headers=( "settle_date", "account", "name", "cp_code", "settlement_amount", "currency", "asset_class", "ids", ), num_format=[("{0:,.2f}", 4)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"Projected Settlements: {fund}", HTMLBody( f""" Hello,

We see the below settlements in the next two days (Positive=Receive, Negative=Pay):

{cls.to_tabulate()} """ ), to_recipients=_settlement_recipients[fund], cc_recipients=_cc_recipients[fund], ) class FxHedge( Monitor, headers=( "date", "account", "amount", "currency", "fund", "category", ), num_format=[("{0:,.2f}", 2)], ): @classmethod def email(cls, fund): if not cls._staging_queue: return cls._em.send_email( f"Projected Hedges: {fund}", HTMLBody( f""" Hello,

Here are the positions we need to hedge:

{cls.to_tabulate()} """ ), to_recipients=("fyu@lmcg.com",), ) class QuantifiMonitor( Monitor, headers=( "uploadtime", "filename", "errors", "warnings", "successes", "total", ), num_format=[], ): @classmethod def email(cls, filename, errors, buf): if not cls._staging_queue: return cls._em.send_email( f"Quantifi Report: {filename} {'**Errors**' if errors else ''}", HTMLBody( f""" {cls.to_tabulate()} """ ), to_recipients=("fyu@lmcg.com",), attach=[FileAttachment(name=filename + ".xml", content=buf)], ) class CitcoMonitor( Monitor, headers=( "process_date", "submit_date", "identifier_type", "citco_id", "serenitas_id", "id", ), num_format=[], ): @classmethod def email(cls, filename, buf): if not cls._staging_queue: return recipients = _recipients["NY_CREW"] action_requested = "" if cls.check_csm(): recipients += ("SYamamiya@citco.com", "DataOpsTC@citco.com") action_requested += "**Action Requested, TradeID Failed**" cls._em.send_email( f"(CITCO) UPLOAD REPORT: {filename} {action_requested}", HTMLBody( f""" {cls.to_tabulate()} """ ), to_recipients=recipients, attach=[FileAttachment(name=filename, content=buf)], ) @classmethod def check_csm(cls): for line in cls._staging_queue: if "TID NOT FOUND" in line[3]: return True