import datetime
from exchangelib import HTMLBody
from tabulate import tabulate
import math
import logging
from dataclasses import dataclass
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_nav) < 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
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 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",
"ghorel@lmcg.com",
"etsui@lmcg.com",
),
attach=[FileAttachment(name=filename + ".xml", content=buf)],
)
class CitcoMonitor(
Monitor,
headers=(
"process_date",
"submit_date",
"identifier_type",
"citco_id",
"serenitas_id",
),
num_format=[],
):
@classmethod
def email(cls, filename, buf):
if not cls._staging_queue:
return
cls._em.send_email(
f"(CITCO) UPLOAD REPORT: {filename} ",
HTMLBody(
f"""
{cls.to_tabulate()}
"""
),
to_recipients=(
"fyu@lmcg.com",
# "ghorel@lmcg.com",
# "etsui@lmcg.com",
),
attach=[FileAttachment(name=filename + ".csv", content=buf)],
)
@dataclass
class EmailOps:
_em = ExchangeMessage()
@classmethod
def email_boston(cls, date):
cls._em.send_email(
f"Missing Cash Balance for Scotia {date}",
f"Please provide cash balance for Scotia for {date} in Blotter.\n\nThanks!",
to_recipients=_recipients["NYOPS"],
)