from dataclasses import field, dataclass
import logging
from typing import Literal, ClassVar
import datetime
import csv
from serenitas.ops.trade_dataclasses import Deal
from serenitas.utils.exchange import ExchangeMessage
from psycopg.errors import UniqueViolation
from exchangelib import HTMLBody
logger = logging.getLogger(__name__)
def get_file_status(s):
is_processed, fname_short = s.rsplit("_", 1)
is_processed = is_processed.rsplit("-")[1] == "PROCESSED"
fname_short = fname_short.removesuffix(".csv")
return is_processed, fname_short
def get_success_data(line):
if line[2]: # This is a trade
identifier_type = "trade"
serenitas_id = line[5]
identifier = line[2]
else:
identifier_type = "instrument"
serenitas_id = line[4]
identifier = line[1]
return identifier_type, serenitas_id, identifier
def get_failed_data(line):
if len(line) == 1:
return ("failed", line[-1])
elif line[1]: # Trade upload
return ("trade", line[2])
elif (
not line[1] and line[2]
): # Instrument upload, just mark as failed if it's a single error message
return ("instrument", line[2])
else:
return ("failed", line[-1])
def instrument_table(instrument_id):
if instrument_id.startswith("IRS"):
return "citco_irs"
elif instrument_id.startswith("SWPO_") or instrument_id.startswith("BNDO_"):
return "citco_swaption"
elif instrument_id.startswith("CDS_"):
return "citco_tranche"
elif instrument_id.startswith("TRS"):
return "citco_trs"
@dataclass
class CitcoSubmission(Deal, deal_type=None, table_name="citco_submission"):
fname: str = field()
identifier_type: Literal["trade", "instrument"]
identifier: str
serenitas_id: str
submit_date: datetime.datetime = field(default=datetime.datetime.now())
@classmethod
def from_citco_line(cls, line, fname):
is_processed, fname_short = get_file_status(fname)
if is_processed:
identifier_type, serenitas_id, identifier = get_success_data(line)
else:
serenitas_id = "failed"
(
identifier_type,
identifier,
) = get_failed_data(line)
return cls(
fname=fname_short,
identifier_type=identifier_type,
identifier=identifier,
serenitas_id=serenitas_id,
)
@classmethod
def process(cls, fh, fname):
next(fh) # skip header
for row in csv.reader(fh):
trade = cls.from_citco_line(row, fname)
trade.stage()
@classmethod
def update_citco_tables(cls):
with cls._conn.cursor() as c:
for row in cls._insert_queue:
if row[1] == "instrument":
serenitas_id = row[4]
c.execute(
f"UPDATE {instrument_table(serenitas_id)} SET committed=True AND status='Acknowledged' WHERE dealid=%s",
(serenitas_id,),
)
@classmethod
def commit(cls):
if not cls._insert_queue:
return
with cls._conn.cursor() as c:
try:
c.executemany(cls._sql_insert, cls._insert_queue)
except UniqueViolation as e:
logger.warning(e)
cls._conn.rollback()
else:
cls._conn.commit()
cls.update_citco_tables()
em = ExchangeMessage()
em.send_email(
f"(CITCO) UPLOAD {'SUCCESS' if cls._insert_queue[0][3] != 'failed' else 'FAILED'}",
"\n".join(map(str, cls._insert_queue)),
(
"fyu@lmcg.com",
"ghorel@lmcg.com",
"etsui@lmcg.com",
),
)
finally:
cls._insert_queue.clear()
_recipients = {
"ISOSEL": (
"simon.oreilly@innocap.com",
"margincalls@innocapglobal.com",
),
"BOWDST": (
"shkumar@sscinc.com",
"hedgemark.lmcg.ops@sscinc.com",
"hm-operations@bnymellon.com",
),
"SERCGMAST": (
"SERENITAS.FA@sscinc.com",
"SERENITAS.ops@sscinc.com",
),
"BAML_FCM": ("fyu@lmcg.com",),
}
@dataclass
class Payment:
settle_date: datetime.date
currency: str
amount: float
_insert_queue: ClassVar[list] = []
@classmethod
def stage_payment(cls, settlements):
for row in settlements:
cls._insert_queue.append(
cls(row.settle_date, row.currency, row.payment_amount)
)
def to_email_format(self):
return f"\t* {self.settle_date}: {self.amount:,.2f} {self.currency}"
class PaymentSettlement(Payment):
@classmethod
def email_innocap(cls, date):
if not cls._insert_queue:
return
em = ExchangeMessage()
em.send_email(
f"Payment Settlements Bond/FX NT: ISOSEL {date}",
"Good morning, \n\nWe have the following amounts settling in the next 4 calendar days at Northern Trust: (Positive Amounts = Receive, Negative Amounts=Pay)\n\n"
+ "\n".join(
settlement.to_email_format() for settlement in cls._insert_queue
),
to_recipients=_recipients["ISOSEL"],
cc_recipients=("Selene-Ops@lmcg.com",),
)
class GFSMonitor(Payment):
@classmethod
def email_globeop(cls, fund):
if not cls._insert_queue:
return
em = ExchangeMessage()
em.send_email(
f"GFS Helper Strategy Issue: {fund}",
"Good morning, \n\nWe noticed some cash in the GFS helper strategy that shouldn't be there:\n\n"
+ "\n".join(
settlement.to_email_format() for settlement in cls._insert_queue
),
to_recipients=_recipients[fund],
cc_recipients=(
"Bowdoin-Ops@LMCG.com" if fund == "BOWDST" else "NYOps@lmcg.com",
),
)
class BamlFcmNotify:
@classmethod
def email_fcm(cls, date, data):
em = ExchangeMessage()
em.send_email(
f"FX Details: 6MZ20049 {date}",
HTMLBody(
f"Hello,
Please see below details for an FX Spot Trade we did with the desk today for account 6MZ20049. Please let me know if you need more information
{data}"
),
to_recipients=_recipients["BAML_FCM"],
cc_recipients=("nyops@lmcg.com",),
)