aboutsummaryrefslogtreecommitdiffstats
path: root/python/ops
diff options
context:
space:
mode:
Diffstat (limited to 'python/ops')
-rw-r--r--python/ops/__init__.py0
-rw-r--r--python/ops/file_gen.py273
-rw-r--r--python/ops/funds.py17
-rw-r--r--python/ops/process_queue.py445
-rw-r--r--python/ops/trade_dataclasses.py1899
5 files changed, 2628 insertions, 6 deletions
diff --git a/python/ops/__init__.py b/python/ops/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/python/ops/__init__.py
diff --git a/python/ops/file_gen.py b/python/ops/file_gen.py
new file mode 100644
index 00000000..1986ba0d
--- /dev/null
+++ b/python/ops/file_gen.py
@@ -0,0 +1,273 @@
+import datetime
+from serenitas.utils.misc import rename_keys
+
+from pyisda.date import previous_twentieth
+from quantlib.time.api import pydate_from_qldate, UnitedStates, Days, Date
+from serenitas.analytics.dates import bus_day
+from .headers import get_headers
+
+
+def get_effective_date(d, swaption_type):
+ if swaption_type == "CD_INDEX_OPTION":
+ return previous_twentieth(d + datetime.timedelta(days=1))
+ else:
+ cal = UnitedStates()
+ return pydate_from_qldate(cal.advance(Date.from_datetime(d), 2, Days))
+
+
+_client_name = {"SERCGMAST": "Serenitas", "BOWDST": "HEDGEMARK", "BRINKER": "LMCG"}
+
+
+def build_line(obj, trade_type="bond", fund="SERCGMAST"):
+ obj["Client"] = _client_name[fund]
+ # Bowdst Globeop has a special short name
+ obj["fund"] = "BOS_PAT_BOWDOIN" if fund == "BOWDST" else fund
+ obj["State"] = "Valid"
+ rename_cols = {
+ "fund": "Fund",
+ "action": "Action",
+ "dealid": "Deal Id",
+ "folder": "Folder",
+ "custodian": "Custodian",
+ "cash_account": "Cash Account",
+ "cp_code": "Counterparty",
+ "identifier": "GlopeOp Security Identifier",
+ "cusip": "CUSIP",
+ "isin": "ISIN",
+ "description": "Security Description",
+ "accrued": "Accrued",
+ "price": "Price",
+ "faceamount": "FaceAmount",
+ "trade_date": "Trade Date",
+ "settle_date": "Settlement Date",
+ "effective_date": "EffectiveDate",
+ "maturity": "MaturityDate",
+ "currency": "Currency",
+ "fixed_rate": "FixedRate",
+ "payment_rolldate": "PaymentRollDateConvention",
+ "day_count": "DayCount",
+ "protection": "Protection",
+ "security_id": "UnderlyingSecurityId",
+ "security_desc": "UnderlyingSecurityDescription",
+ "upfront": "UpfrontFee",
+ "upfront_settle_date": "UpfrontFeePayDate",
+ "swap_type": "SwapType",
+ "orig_attach": "AttachmentPoint",
+ "orig_detach": "ExhaustionPoint",
+ "clearing_facility": "Clearing Facility",
+ "isda_definition": "ISDADefinition",
+ "expiration_date": "ExpirationDate",
+ "portfolio": "Portfolio",
+ "settlement_type": "SettlementMode",
+ "principal_payment": "PrincipalPayment",
+ "accrued_payment": "AccruedPayment",
+ "current_face": "CurrentFace",
+ }
+ rename_cols[
+ "curr_notional" if fund in ("SERCGMAST", "BOWDST") else "notional"
+ ] = "Notional"
+ rename_keys(obj, rename_cols)
+ if trade_type in ("bond", "swaption", "future"):
+ obj["Transaction Indicator"] = "Buy" if obj["buysell"] else "Sell"
+ if trade_type == "bond":
+ obj["Deal Type"] = "MortgageDeal"
+ obj["Portfolio"] = "MORTGAGES"
+ obj["Delivery"] = "S"
+ # zero coupon bond
+ if obj["CUSIP"] != obj["GlopeOp Security Identifier"]:
+ obj["CUSIP"] = None
+ elif trade_type == "swaption":
+ obj["Deal Type"] = "SwaptionDeal"
+ obj["ExerciseType"] = "European"
+ rename_keys(
+ obj,
+ {
+ "Settlement Date": "PremiumSettlementDate",
+ "notional": "Notional",
+ "initial_margin_percentage": "InitialMarginPercentage",
+ },
+ )
+ obj["PremiumSettlementAmount"] = (
+ obj["Price"] * obj["Notional"] * obj.get("factor", 1.0) * 0.01
+ )
+ obj["PremiumSettlementCurrency"] = obj["Currency"]
+ obj["RegenerateCashFlow"] = "N"
+ for direction in ["Pay", "Receive"]:
+ obj[direction + "MaturityDate"] = obj["MaturityDate"]
+ obj[direction + "Currency"] = obj["Currency"]
+ obj[direction + "Notional"] = obj["Notional"]
+ obj[direction + "EffectiveDate"] = get_effective_date(
+ obj["ExpirationDate"], obj["SwapType"]
+ )
+ if obj["SwapType"] == "CD_INDEX_OPTION":
+ for direction in ["Pay", "Receive"]:
+ obj[direction + "Daycount"] = "ACT/360"
+ obj[direction + "Frequency"] = "Quarterly"
+ obj[direction + "PaymentRollConvention"] = "Following"
+
+ for leg_type in ["Receive", "Pay"]:
+ obj[leg_type + "LegRateType"] = "Fixed"
+ if obj["option_type"] == "PAYER":
+ obj["ReceiveFixedRate"] = 0.0
+ obj["PayFixedRate"] = obj["FixedRate"]
+ elif obj["option_type"] == "RECEIVER":
+ obj["PayFixedRate"] = 0.0
+ obj["ReceiveFixedRate"] = obj["FixedRate"]
+ elif obj["SwapType"] == "SWAPTION":
+ for direction in ["Pay", "Receive"]:
+ obj[direction + "PaymentRollConvention"] = "ModifiedFollowing"
+ if obj["option_type"] == "RECEIVER":
+ fixed, floating = "Receive", "Pay"
+ else:
+ fixed, floating = "Pay", "Receive"
+ # fixed leg
+ obj[fixed + "Frequency"] = "Yearly"
+ obj[fixed + "Daycount"] = "ACT/360"
+ obj[fixed + "FixedRate"] = obj["strike"]
+ obj[fixed + "LegRateType"] = "Fixed"
+ obj[fixed + "InterestCalcMethod"] = "Simple Interest"
+ # floating leg
+ obj[floating + "Frequency"] = "Yearly"
+ obj[floating + "Daycount"] = "ACT/360"
+ obj[floating + "LegRateType"] = "Float"
+ obj[floating + "FloatRate"] = "SOFRINDX"
+ obj[floating + "InterestCalcMethod"] = "Simple Interest"
+
+ else:
+ raise ValueError(
+ "'SwapType' needs to be one of 'CD_INDEX_OPTION' or 'SWAPTION'"
+ )
+
+ obj["PremiumCurrency"] = obj["Currency"]
+ if obj["InitialMarginPercentage"]:
+ obj["InitialMarginCurrency"] = obj["Currency"]
+ obj["UnderlyingInstrument"] = obj.pop("UnderlyingSecurityId")
+ if obj["SwapType"] == "CD_INDEX_OPTION":
+ obj["Strike"] = obj.pop("strike")
+
+ elif trade_type == "cds":
+ freq = {4: "Quarterly", 12: "Monthly"}
+ obj["Deal Type"] = "CreditDefaultSwapDeal"
+ obj["PaymentFrequency"] = freq[obj["frequency"]]
+ obj["InitialMarginPercentage"] = obj.pop("initial_margin_percentage")
+ if obj["InitialMarginPercentage"]:
+ obj["InitialMarginCurrency"] = obj["Currency"]
+ if obj["Clearing Facility"] is None:
+ obj["Clearing Facility"] = "NOT CLEARED"
+
+ elif trade_type == "future":
+ obj["Deal Type"] = "FutureDeal"
+ rename_keys(
+ obj,
+ {
+ "commission": "Commission",
+ "quantity": "Quantity",
+ "swap_type": "Swap Type",
+ "bbg_ticker": "Bloomberg Ticker",
+ "Currency": "Trade Currency",
+ "exchange": "Exchange",
+ },
+ )
+ elif trade_type == "wire":
+ obj["Deal Type"] = "CashFlowDeal"
+ obj["Transaction Type"] = "Transfer"
+ obj["Instrument Type"] = "Cashflow"
+ obj["Settlement Date"] = obj["Trade Date"]
+ strat_portfolio_map = {
+ "IGOPTDEL": "OPTIONS",
+ "COCSH": "OPTIONS",
+ "IGINX": "TRANCHE",
+ "BSPK": "TRANCHE",
+ "TCSH": "TRANCHE",
+ "SER_ITRXCURVE": "SERG__CURVE",
+ "XCURVE": "SERG__CURVE",
+ "M_CSH_CASH": "CASH",
+ "CVECSH": "SERG__CURVE",
+ "SER_ITRXCVCSH": "SERG__CURVE",
+ }
+ obj["Portfolio"] = strat_portfolio_map.get(obj["Folder"])
+ rename_keys(obj, {"amount": "Amount"})
+
+ elif trade_type == "spot":
+ standard_settle = (obj["Trade Date"] + 2 * bus_day).date()
+ if obj["Settlement Date"] > standard_settle:
+ obj["Deal Type"] = "ForwardDeal"
+ fx_rate = "Forward Rate"
+ else:
+ obj["Deal Type"] = "SpotDeal"
+ fx_rate = "Spot Rate"
+ rename_keys(
+ obj,
+ {
+ "commission": "Commission",
+ "commission_currency": "Commission Currency",
+ "sell_currency": "Sell Currency",
+ "sell_amount": "Sell Amount",
+ "buy_currency": "Buy Currency",
+ "buy_amount": "Buy Amount",
+ "spot_rate": fx_rate,
+ },
+ )
+ elif trade_type == "fx_swap":
+ obj["Deal Type"] = "FxSwapDeal"
+ obj["Action"] = "NEW"
+ rename_keys(
+ obj,
+ {
+ "near_rate": "Near Side Currency Rate",
+ "near_settle_date": "Near Side Settlement Date",
+ "near_buy_currency": "Near Side Buy Currency",
+ "near_buy_amount": "Near Side Buy Amount",
+ "near_sell_currency": "Near Side Sell Currency",
+ "near_sell_amount": "Near Side Sell Amount",
+ "far_rate": "Far Side Rate",
+ "far_settle_date": "Far Side Settlement Date",
+ "far_buy_currency": "Far Side Buy Currency",
+ "far_buy_amount": "Far Side Buy Amount",
+ "far_sell_currency": "Far Side Sell Currency",
+ "far_sell_amount": "Far Side Sell Amount",
+ },
+ )
+ elif trade_type == "repo":
+ obj["Deal Type"] = "RepoDeal"
+ obj["OpenRepo"] = "Y" if obj["open_repo"] else "N"
+ rename_keys(
+ obj,
+ {
+ "weighted_amount": "WeightedAmount",
+ "repo_rate": "RepoRate",
+ "transaction_indicator": "TransactionIndicator",
+ },
+ )
+ elif trade_type == "capfloor":
+ obj["Deal Type"] = "CapFloorDeal"
+ obj["PaymentBDC"] = obj["bdc_convention"]
+ obj["AccrualBDC"] = obj["bdc_convention"]
+ obj["MaturityBDC"] = "NONE"
+ obj["TransactionIndicator"] = "Buy" if obj["buysell"] else "Sell"
+ # missing data : 'Adjusted', 'RollConvention', 'Calendar', 'Arrears', 'Collateralized', 'MaturityBDC'
+ rename_keys(
+ obj,
+ {
+ "comments": "Comments",
+ "floating_rate_index_desc": "FloatingRateIndex",
+ "cap_or_floor": "CapOrFloor",
+ "amount": "Notional",
+ "strike": "Strike",
+ "value_date": "ValueDate",
+ "expiration_date": "MaturityDate",
+ "premium_percent": "PremiumPercent",
+ "pricing_type": "PricingType",
+ "payment_frequency": "PaymentFrequency",
+ "fixing_frequency": "FixingFrequency",
+ "day_count_convention": "Basis",
+ "bdc_convention": "PaymentBDC",
+ "payment_mode": "PaymentMode",
+ "payment_at_beginning_or_end": "PaymentAtBeginningOrEnd",
+ "initial_margin_percentage": "InitialMarginPercentage",
+ "intial_margin_currency": "InitialMarginCurrency",
+ "reset_lag": "ResetLag",
+ "swap_type": "SwapType",
+ },
+ )
+ return obj
diff --git a/python/ops/funds.py b/python/ops/funds.py
index bfc3750d..aaea047e 100644
--- a/python/ops/funds.py
+++ b/python/ops/funds.py
@@ -5,6 +5,8 @@ from serenitas.utils.remote import FtpClient, SftpClient
from serenitas.utils.exchange import ExchangeMessage, FileAttachment
from io import StringIO
from typing import Tuple, Union
+from serenitas.utils.env import DAILY_DIR
+from .file_gen import get_headers, build_line
class Fund:
@@ -23,8 +25,11 @@ class Fund:
def build_buffer(cls, trade_type):
buf = StringIO()
csvwriter = csv.writer(buf)
- csvwriter.writerow(get_headers(trade_type, cls.name))
- csvwriter.writerows(cls.staged_queue)
+ headers = get_headers(trade_type, cls.name)
+ csvwriter.writerow(headers)
+ csvwriter.writerows(
+ [[obj.get(h) for h in headers] for obj in cls.stating_queue]
+ )
buf = buf.getvalue().encode()
dest = cls.get_filepath(DAILY_DIR, trade_type)
dest.parent.mkdir(exist_ok=True)
@@ -36,8 +41,8 @@ class Fund:
cls.headers = get_headers(trade_type, cls.name)
@classmethod
- def stage(cls, trade, trade_type):
- cls.staged_queue.append(build_line(trade, trade_type, cls.name))
+ def stage(cls, trade, *, trade_type, **kwargs):
+ cls.stating_queue.append(build_line(trade, trade_type, cls.name))
@classmethod
def get_filepath(
@@ -110,5 +115,5 @@ class Bowdst(Fund, fund_name="BOWDST"):
class Selene(Fund, fund_name="ISOSEL"):
@classmethod
- def stage(cls, trade, action="NEW"):
- cls.staged_queue.append(trade.to_citco(action))
+ def stage(cls, trade, *, action="NEW", **kwargs):
+ cls.stating_queue.append(trade.to_citco(action))
diff --git a/python/ops/process_queue.py b/python/ops/process_queue.py
new file mode 100644
index 00000000..8d88bda3
--- /dev/null
+++ b/python/ops/process_queue.py
@@ -0,0 +1,445 @@
+import argparse
+import blpapi
+import logging
+import psycopg
+import pathlib
+import re
+import redis
+import sys
+
+from serenitas.analytics.api import CreditIndex
+
+try:
+ from serenitas.utils.env import DAILY_DIR
+except KeyError:
+ sys.exit("Please set path of daily directory in 'SERENITAS_DAILY_DIR'")
+
+from collections import defaultdict
+from pickle import dumps, loads
+from serenitas.analytics.bbg_helpers import init_bbg_session, retrieve_data
+
+from serenitas.utils import get_redis_queue
+from tabulate import tabulate
+from .funds import Fund
+from .trade_dataclasses import DealKind
+
+
+def groupby(p: redis.client.Pipeline, key: str, trade_key: str):
+ d = defaultdict(list)
+ for buf in p.lrange(key, 0, -1):
+ trade = loads(buf)
+ d[trade[trade_key]].append(trade)
+ return d
+
+
+def get_trades(p: redis.client.Pipeline, key: str):
+ for tradeid, trades in groupby(p, key, "id").items():
+ if len(trades) == 1:
+ yield trades[0]
+ else:
+ if trades[-1]["action"] == "CANCEL":
+ continue
+ if trades[0]["action"] == "NEW":
+ trades[-1]["action"] = "NEW"
+ yield trades[-1]
+ if trades[-1]["action"] == "UPDATE":
+ yield trades[-1]
+
+
+def process_indicative(
+ p: redis.client.Pipeline,
+ trade_type: str,
+ upload: bool,
+ session: blpapi.session.Session,
+ conn: psycopg.Connection,
+) -> None:
+ process_fun = globals().get(
+ f"{trade_type}_trade_process", lambda conn, session, trade: trade
+ )
+ for trade in get_trades(p, trade_type):
+ process_fun(conn, session, trade)
+ fund = trade["fund"]
+ if trade.get("upload", True) and (
+ fund in ("SERCGMAST", "BOWDST") or trade_type in ("cds", "swaption")
+ ):
+ p.rpush(f"{trade_type}_upload", dumps(trade))
+ if trade.get("swap_type", None) in (
+ "CD_INDEX_OPTION",
+ "CD_INDEX_TRANCHE",
+ "BESPOKE",
+ ):
+ DealKind[trade_type].from_dict(**trade).mtm_stage()
+ if Deal := DealKind[trade_type]:
+ Deal.mtm_upload()
+ p.delete(trade_type)
+
+
+def process_upload(
+ p: redis.client.Pipeline,
+ trade_type: str,
+ upload: bool,
+) -> None:
+ key = f"{trade_type}_upload"
+ for fund_name, l in groupby(p, key, "fund").items():
+ fund = Fund[fund_name]()
+ for trade in l:
+ fund.stage(trade, trade_type=trade_type)
+ buf, dest = fund.build_buffer(trade_type)
+ if upload:
+ fund.upload(buf, dest.name)
+ p.delete(key)
+
+
+def terminate_list(
+ p: redis.client.Pipeline,
+ key: str,
+ upload: bool,
+ conn: psycopg.connection,
+ base_dir: pathlib.Path = DAILY_DIR,
+):
+ trade_type, fund, _ = key.split("_")
+ terms = []
+ for term in p.lrange(key, 0, -1):
+ termination = loads(term)
+ DealKind["termination"].from_dict(**termination).mtm_stage()
+ try:
+ terms.append(termination.to_globeop())
+ except TypeError as e:
+ logging.error(e)
+ return
+ DealKind["termination"].mtm_upload()
+ if upload and terms:
+ f = Fund[fund]()
+ f.staging_queue = terms
+ dest = f.get_filepath(base_dir, (trade_type, "A"))
+ buf = f.build_buffer("termination")
+ f.upload(buf, dest)
+ p.delete(key)
+
+
+def get_bbg_data(
+ conn,
+ session,
+ identifier,
+ cusip=None,
+ isin=None,
+ settle_date=None,
+ asset_class=None,
+ **kwargs,
+):
+ fields = ["MTG_FACTOR_SET_DT", "INT_ACC", "ISSUER"]
+ fields_dict = {
+ "Mtge": ["MTG_FACE_AMT", "START_ACC_DT"],
+ "Corp": ["AMT_ISSUED", "PREV_CPN_DT"],
+ }
+ with conn.cursor() as c:
+ c.execute(
+ "SELECT identifier FROM securities WHERE identifier=%s", (identifier,)
+ )
+ if not c.fetchone():
+ fields += [
+ "MATURITY",
+ "CRNCY",
+ "NAME",
+ "FLOATER",
+ "FLT_SPREAD",
+ "CPN",
+ "CPN_TYP",
+ "CPN_FREQ",
+ "FIRST_CPN_DT",
+ "MTG_PAY_DELAY",
+ "DAY_CNT_DES",
+ "NOMINAL_PAYMENT_DAY",
+ "ISSUE_DT",
+ "RESET_IDX",
+ "ID_BB_GLOBAL",
+ ]
+
+ cusip_or_isin = cusip or isin
+ for bbg_type in ["Mtge", "Corp"]:
+ bbg_id = cusip_or_isin + " " + bbg_type
+ data = retrieve_data(
+ session,
+ [bbg_id],
+ fields + fields_dict[bbg_type],
+ overrides={"SETTLE_DT": settle_date} if settle_date else None,
+ )
+ if data[bbg_id]:
+ break
+ else:
+ logging.error(f"{cusip_or_isin} not in bloomberg")
+ return
+
+ bbg_data = data[bbg_id]
+ if bbg_data.get("MTG_FACTOR_SET_DT", 0) == 0:
+ bbg_data["MTG_FACTOR_SET_DT"] = 1
+ bbg_data["INT_ACC"] = 0
+ if len(fields) > 3: # we don't have the data in the securities table
+ sql_fields = [
+ "identifier",
+ "cusip",
+ "isin",
+ "description",
+ "face_amount",
+ "maturity",
+ "floater",
+ "spread",
+ "coupon",
+ "frequency",
+ "day_count",
+ "first_coupon_date",
+ "pay_delay",
+ "currency",
+ "bbg_type",
+ "asset_class",
+ "start_accrued_date",
+ "issuer",
+ "reset_index",
+ "coupon_type",
+ "payment_day",
+ "issue_date",
+ "figi",
+ ]
+ placeholders = ",".join(["%s"] * len(sql_fields))
+ columns = ",".join(sql_fields)
+
+ sqlstr = (
+ f"INSERT INTO securities({columns}) VALUES({placeholders}) "
+ "ON CONFLICT (identifier) DO NOTHING"
+ )
+ isfloater = bbg_data["FLOATER"] == "Y"
+ pay_delay = bbg_data.get("MTG_PAY_DELAY", 0)
+ day_count = bbg_data.get("DAY_CNT_DES")
+ if m := re.match(r"[^(\s]+", day_count):
+ day_count = m.group(0)
+ if isinstance(pay_delay, str):
+ pay_delay = int(pay_delay.split(" ")[0])
+ with conn.cursor() as c:
+ c.execute(
+ sqlstr,
+ (
+ identifier,
+ cusip,
+ isin,
+ bbg_data["NAME"],
+ bbg_data.get("MTG_FACE_AMT") or bbg_data.get("AMT_ISSUED"),
+ bbg_data.get("MATURITY"),
+ isfloater,
+ bbg_data.get("FLT_SPREAD") if isfloater else None,
+ bbg_data.get("CPN") if not isfloater else None,
+ bbg_data.get("CPN_FREQ"),
+ day_count,
+ bbg_data.get("FIRST_CPN_DT"),
+ pay_delay,
+ bbg_data.get("CRNCY"),
+ bbg_type,
+ asset_class,
+ bbg_data.get("START_ACC_DT") or bbg_data.get("PREV_CPN_DT"),
+ bbg_data["ISSUER"],
+ bbg_data.get("RESET_IDX"),
+ bbg_data["CPN_TYP"],
+ bbg_data["NOMINAL_PAYMENT_DAY"],
+ bbg_data["ISSUE_DT"],
+ bbg_data["ID_BB_GLOBAL"],
+ ),
+ )
+ conn.commit()
+ return bbg_data
+
+
+def bond_trade_process(conn, session, trade):
+ bbg_data = get_bbg_data(conn, session, **trade)
+ currentface = trade["CurrentFace"] = (
+ trade["faceamount"] * bbg_data["MTG_FACTOR_SET_DT"]
+ )
+ accrued_payment = trade["AccruedPayment"] = (
+ bbg_data["INT_ACC"] * currentface / 100.0
+ )
+ principal_payment = trade["PrincipalPayment"] = currentface * trade["price"] / 100.0
+ if trade["accrued"] is None:
+ trade["accrued"] = bbg_data["INT_ACC"]
+ else:
+ if trade["accrued"] != bbg_data["INT_ACC"]:
+ logging.error(
+ f"{trade['accrued']} does not match bbg amount of {bbg_data['INT_ACC']}"
+ )
+
+ with conn.cursor() as c:
+ c.execute(
+ "UPDATE bonds SET principal_payment = %s, accrued_payment = %s, accrued=%s "
+ "WHERE id = %s",
+ (principal_payment, accrued_payment, trade["accrued"], int(trade["id"])),
+ )
+ # mark it at buy price
+ if trade["buysell"]:
+ sqlstr = "INSERT INTO marks VALUES(%s, %s, %s) ON CONFLICT DO NOTHING"
+ with conn.cursor() as c:
+ c.execute(
+ sqlstr, (trade["trade_date"], trade["identifier"], trade["price"])
+ )
+ conn.commit()
+ return trade
+
+
+def is_tranche_trade(trade):
+ return trade["swap_type"] in ("CD_INDEX_TRANCHE", "BESPOKE")
+
+
+def swaption_trade_process(conn, session, trade):
+ sqlstr = (
+ "SELECT indexfactor/100 FROM index_version "
+ "WHERE redindexcode=%(security_id)s"
+ )
+ try:
+ with conn.cursor() as c:
+ c.execute(sqlstr, trade)
+ (factor,) = c.fetchone()
+ except ValueError as e:
+ logging.error(e)
+ return trade
+ except TypeError:
+ # factor missing, probably IR swaption
+ pass
+ else:
+ trade["factor"] = factor
+ finally:
+ if trade["option_type"] == "RECEIVER":
+ trade["OptionType"] = "Call"
+ elif trade["option_type"] == "PAYER":
+ trade["OptionType"] = "Put"
+ return trade
+
+
+def cds_trade_process(conn, session, trade):
+ sqlstr = (
+ "SELECT indexfactor/100 FROM index_version "
+ "WHERE redindexcode=%(security_id)s"
+ )
+ try:
+ with conn.cursor() as c:
+ c.execute(sqlstr, trade)
+ (factor,) = c.fetchone()
+ except ValueError:
+ bbg_data = get_bbg_data(
+ conn,
+ session,
+ trade["security_id"],
+ isin=trade["security_id"],
+ asset_class="Subprime",
+ )
+
+ factor = bbg_data["MTG_FACTOR_SET_DT"]
+ if is_tranche_trade(trade):
+ tranche_factor = (trade["attach"] - trade["detach"]) / (
+ trade["orig_attach"] - trade["orig_detach"]
+ )
+ trade["curr_notional"] = trade["notional"] * tranche_factor
+ trade["Factor"] = tranche_factor
+ else:
+ trade["curr_notional"] = trade["notional"] * factor
+ trade["Factor"] = factor
+ if trade["upfront"]:
+ return trade
+ index = CreditIndex(
+ redcode=trade["security_id"],
+ maturity=trade["maturity"],
+ notional=trade["notional"],
+ value_date=trade["trade_date"],
+ )
+ index.direction = trade["protection"]
+ with conn.cursor() as c:
+ if trade["traded_level"]:
+ if not is_tranche_trade(trade):
+ index.ref = float(trade["traded_level"])
+ trade["upfront"] = -index.pv
+ else:
+ accrued = index._accrued * trade["fixed_rate"]
+ match index.index_type:
+ case "HY":
+ dirty_price = float(trade["traded_level"]) + accrued
+ trade["upfront"] = (
+ -(100 - dirty_price)
+ * index.notional
+ * trade["Factor"]
+ * 0.01
+ )
+ case "EU" | "XO" if trade["orig_attach"] in (6, 12, 35):
+ if trade["orig_attach"] == 6:
+ index.recovery = 0.0
+ index.spread = float(trade["traded_level"])
+ trade["upfront"] = (
+ -index._pv * trade["notional"] * trade["Factor"]
+ )
+ case _:
+ dirty_protection = float(trade["traded_level"]) - accrued
+ trade["upfront"] = (
+ -dirty_protection * index.notional * trade["Factor"] * 0.01
+ )
+ c.execute(
+ "UPDATE cds SET upfront=%s WHERE dealid=%s",
+ (trade["upfront"], trade["dealid"]),
+ )
+
+ else:
+ index.pv = -trade["upfront"]
+ trade["traded_level"] = index.ref
+ c.execute(
+ "UPDATE cds SET traded_level=%s WHERE dealid=%s",
+ (trade["traded_level"], trade["dealid"]),
+ )
+ conn.commit()
+ return trade
+
+
+def print_trade(trade):
+ d = trade.copy()
+ d["buysell"] = "Buy" if d["buysell"] else "Sell"
+ return tabulate((k, v) for k, v in d.items())
+
+
+if __name__ == "__main__":
+ import os
+
+ os.environ["SERENITAS_APP_NAME"] = "process_queue"
+ from functools import partial
+ from serenitas.utils.pool import dawn_pool
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-n", "--no-upload", action="store_true", help="do not upload to Globeop"
+ )
+ args = parser.parse_args()
+ r = get_redis_queue()
+ with dawn_pool.connection() as conn, init_bbg_session() as session:
+ for trade_type in [
+ "cds",
+ "swaption",
+ "repo",
+ "future",
+ "wire",
+ "spot",
+ "fx_swap",
+ "capfloor",
+ ]:
+ p_list = partial(
+ process_indicative,
+ trade_type=trade_type,
+ upload=not args.no_upload,
+ session=session,
+ conn=conn,
+ )
+ r.transaction(p_list, trade_type)
+ p_upload = partial(
+ process_upload,
+ trade_type=trade_type,
+ upload=not args.no_upload,
+ )
+ r.transaction(p_upload, trade_type)
+
+ for trade_type in ("cds", "swaption", "capfloor"):
+ for fund in ("SERCGMAST", "BOWDST", "BRINKER"):
+ key = f"{trade_type}_{fund}_termination"
+ t_list = partial(
+ terminate_list, key=key, upload=not args.no_upload, conn=conn
+ )
+ r.transaction(t_list, key)
diff --git a/python/ops/trade_dataclasses.py b/python/ops/trade_dataclasses.py
new file mode 100644
index 00000000..c3d732e1
--- /dev/null
+++ b/python/ops/trade_dataclasses.py
@@ -0,0 +1,1899 @@
+from dataclasses import dataclass, field, fields, Field
+from enum import Enum
+from io import StringIO
+from headers import DealType, MTM_HEADERS, HEADERS
+from csv_headers.citco import GIL, GTL
+from typing import ClassVar, Tuple, Union
+from decimal import Decimal
+from typing import Literal
+import csv
+import datetime
+from psycopg.types.numeric import Int2BinaryDumper
+from psycopg import adapters
+from serenitas.analytics.dates import (
+ next_business_day,
+ previous_twentieth,
+ adjust_next_business_day,
+ prev_business_day,
+)
+from serenitas.utils.db2 import dbconn
+from serenitas.utils.env import DAILY_DIR
+from serenitas.utils.remote import FtpClient, SftpClient
+from lru import LRU
+
+from psycopg.errors import UniqueViolation
+import logging
+import warnings
+
+
+logger = logging.getLogger(__name__)
+Fund = Literal["SERCGMAST", "BRINKER", "BOWDST"]
+Portfolio = Literal[
+ "OPTIONS", "IR", "MORTGAGES", "CURVE", "TRANCHE", "CLO", "HEDGE_MAC"
+] # deprecated IG, HY, STRUCTURED
+
+_funds = {"BAML": "SERCGMAST", "GS": "BOWDST", "WF": "SERCGMAST"}
+_fcms = {
+ "Bank of America, N.A.": "BAML",
+ "Goldman Sachs": "GS",
+ "BOA": "BAML",
+ "GOLD": "GS",
+ "Wells Fargo Secs": "WF",
+}
+
+_client_name = {"SERCGMAST": "Serenitas", "BOWDST": "HEDGEMARK", "BRINKER": "LMCG"}
+
+
+class BusDayConvention(str, Enum):
+ modified_following = "Modified Following"
+ following = "Following"
+ modified_preceding = "Modified Preceding"
+ second_day_after = "Second-Day-After"
+ end_of_month = "End-of-Month"
+
+
+DayCount = Literal["ACT/360", "ACT/ACT", "30/360", "ACT/365"]
+
+IsdaDoc = Literal["ISDA2014", "ISDA2003Cred"]
+
+
+class Frequency(Enum):
+ Quarterly = 4
+ Monthly = 12
+
+
+Ccy = Literal["USD", "CAD", "EUR", "YEN"]
+
+
+SwapType = Literal[
+ "CD_INDEX", "CD_INDEX_TRANCHE", "CD_BASKET_TRANCHE", "ABS_CDS", "BESPOKE"
+]
+
+OptionType = Literal["RECEIVER", "PAYER"]
+ClearingFacility = Literal["ICE-CREDIT", "NOT CLEARED"]
+CdsStrat = Literal[
+ "HEDGE_CSO",
+ "HEDGE_CLO",
+ "HEDGE_MAC",
+ "HEDGE_MBS",
+ "SER_IGSNR",
+ "SER_IGMEZ",
+ "SER_IGEQY",
+ "SER_IGINX",
+ "SER_HYSNR",
+ "SER_HYMEZ",
+ "SER_HYEQY",
+ "SER_HYINX",
+ "SER_HYCURVE",
+ "SER_IGCURVE",
+ "SER_ITRXCURVE",
+ "XCURVE",
+ "MBSCDS",
+ "IGOPTDEL",
+ "HYOPTDEL",
+ "HYEQY",
+ "HYMEZ",
+ "HYSNR",
+ "HYINX",
+ "IGEQY",
+ "IGMEZ",
+ "IGSNR",
+ "IGINX",
+ "XOEQY",
+ "XOMEZ",
+ "XOINX",
+ "EUEQY",
+ "EUMEZ",
+ "EUSNR",
+ "EUINX",
+ "BSPK",
+ "*",
+]
+BondStrat = Literal[
+ "M_STR_MAV",
+ "M_STR_MEZZ",
+ "CSO_TRANCH",
+ "M_CLO_BB20",
+ "M_CLO_AAA",
+ "M_CLO_BBB",
+ "M_MTG_IO",
+ "M_MTG_THRU",
+ "M_MTG_GOOD",
+ "M_MTG_B4PR",
+ "M_MTG_RW",
+ "M_MTG_FP",
+ "M_MTG_LMG",
+ "M_MTG_SD",
+ "M_MTG_PR",
+ "M_MTG_CRT_SD",
+ "CRT_LD",
+ "CRT_LD_JNR",
+ "CRT_SD",
+ "IGNORE",
+ "MTG_REPO",
+]
+
+SwaptionStrat = Literal[
+ "IGPAYER",
+ "IGREC",
+ "HYPAYER",
+ "HYREC",
+ "STEEP",
+ "DV01",
+ "HEDGE_MAC",
+]
+
+SpotStrat = Literal[
+ "M_STR_MAV", "M_STR_MEZZ", "SER_IRTXCURVE", "M_CSH_CASH", "TCSH", "*"
+]
+AssetClass = Literal["CSO", "Subprime", "CLO", "CRT"]
+
+
+@dataclass
+class Counterparty:
+ name: str
+
+
+class FrequencyDumper(Int2BinaryDumper):
+ def dump(self, f):
+ return super().dump(f.value)
+
+
+adapters.register_dumper(Frequency, FrequencyDumper)
+
+
+def desc_str(index_type, series, tenor):
+ if index_type in ("IG", "HY", "HYBB"):
+ return f"CDX {index_type} CDSI S{series} {tenor}Y"
+ elif index_type == "XO":
+ return f"ITRX XOVER CDSI S{series} {tenor}Y"
+ elif index_type == "EU":
+ return f"ITRX EUR CDSI S{series} {tenor}Y"
+
+
+def is_default_init_field(cls, attr):
+ match getattr(cls, attr, None):
+ case Field(init=False):
+ return False
+ case _:
+ return True
+
+
+class DealKind:
+ def __class_getitem__(cls, trade_type: str):
+ match trade_type:
+ case "cds":
+ return CDSDeal
+ case "swaption":
+ return SwaptionDeal
+ case "termination":
+ return TerminationDeal
+ case _:
+ return None
+
+
+def get_admin_headers(fund, trade_type):
+ if fund in ("SERCGMAST", "BOWDST", "BRINKER"):
+ try:
+ return HEADERS[trade_type]
+ except:
+ from headers.globeop_upload import globeop_IRS, globeop_TRS
+
+ return globeop_TRS
+
+
+def get_fname(
+ trade_type: Union[str, Tuple[str, str]],
+ fund: str = "SERCGMAST",
+):
+ d = {
+ "bond": "Mortgages",
+ "cds": "CreditDefaultSwapDeal",
+ "swaption": "SwaptionDeal",
+ "future": "Future",
+ "wire": "CashFlowDeal",
+ "spot": "SpotDeal",
+ "fx_swap": "FxSwapDeal",
+ "capfloor": "CapFloor",
+ "repo": "RepoDeal",
+ "termination": "Termination",
+ "trs": "TRS",
+ "irs": "IRS",
+ }
+ trade_tag: str
+ if isinstance(trade_type, tuple):
+ trade_tag = d[trade_type[0]] + trade_type[1]
+ else:
+ trade_tag = d[trade_type]
+
+ timestamp = datetime.datetime.now()
+ if fund == "BRINKER":
+ return f"LMCG_BBH_SWAP_TRADES_P.{timestamp:%Y%m%d%H%M%S}.csv"
+ elif fund == "SERCGMAST":
+ return f"Serenitas.ALL.{timestamp:%Y%m%d.%H%M%S}.{trade_tag}.csv"
+ elif fund == "BOWDST":
+ return f"Bowdst.ALL.{timestamp:%Y%m%d.%H%M%S}.{trade_tag}.csv"
+
+
+def upload_buf(buf, dest, fund):
+ match fund:
+ case "SERCGMAST":
+ ftp = FtpClient.from_creds("globeop")
+ ftp.client.cwd("incoming")
+ ftp.put(buf, dest)
+ case "BOWDST":
+ sftp = SftpClient.from_creds("hm_globeop")
+ sftp.put(buf, dest)
+ case "BRINKER":
+ sftp = SftpClient.from_creds("bbh")
+ sftp.put(buf, dest)
+
+
+class Deal:
+ _conn: ClassVar = dbconn("dawndb", application_name="autobooker")
+ _registry = {}
+ _table_name: None
+ _sql_fields: ClassVar[list[str]]
+ _sql_insert: ClassVar[str]
+ _sql_select: ClassVar[str]
+ _insert_queue: ClassVar[list] = []
+ _admin_queue: ClassVar[list] = []
+
+ def __class_getitem__(cls, deal_type: DealType):
+ return cls._registry[deal_type]
+
+ def __init_subclass__(
+ cls, deal_type: DealType, table_name: str, insert_ignore=(), **kwargs
+ ):
+ super().__init_subclass__(**kwargs)
+ cls._registry[deal_type] = cls
+ cls._table_name = table_name
+ insert_columns = [c for c in cls.__annotations__ if c not in insert_ignore]
+ place_holders = ",".join(["%s"] * len(insert_columns))
+ cls._sql_fields = {
+ c: None for c in cls.__annotations__ if is_default_init_field(cls, c)
+ }
+
+ cls._sql_insert = f"INSERT INTO {table_name}({','.join(insert_columns)}) VALUES({place_holders})"
+ cls._sql_select = (
+ f"SELECT {','.join(cls._sql_fields)} FROM {table_name} WHERE id=%s"
+ )
+
+ def stage(self):
+ self._insert_queue.append(
+ [
+ getattr(self, f.name)
+ for f in fields(self)
+ if f.metadata.get("insert", True)
+ ]
+ )
+
+ @classmethod
+ def admin_upload(cls, fund, trade_type, upload):
+ if not cls._admin_queue: # early exit
+ return
+ buf = StringIO()
+ csvwriter = csv.writer(buf)
+ headers = get_admin_headers(fund, trade_type)
+ csvwriter.writerow(headers)
+ csvwriter.writerows(
+ [row.get(h, None) for h in headers] for row in cls._admin_queue
+ )
+ buf = buf.getvalue().encode()
+ fname = get_fname(trade_type, fund)
+ dest = DAILY_DIR / str(datetime.date.today()) / fname
+ dest.parent.mkdir(exist_ok=True)
+ dest.write_bytes(buf)
+ if upload:
+ upload_buf(buf, fname, fund)
+
+ def admin_stage(self):
+ self._admin_queue.append(self.to_globeop())
+
+ @classmethod
+ def commit(cls):
+ with cls._conn.cursor() as c:
+ c.executemany(cls._sql_insert, cls._insert_queue)
+ cls._conn.commit()
+ cls._insert_queue.clear()
+
+ @classmethod
+ def from_tradeid(cls, trade_id: int):
+ with cls._conn.cursor() as c:
+ c.execute(cls._sql_select, (trade_id,))
+ r = c.fetchone()
+ return cls(*r)
+
+ def serialize(self, tag: str):
+ return {
+ f.metadata.get(tag, f.name): getattr(self, f.name) for f in fields(self)
+ }
+
+
+class BbgDeal:
+ _bbg_insert_queue: ClassVar[list] = []
+ _cache: ClassVar[LRU] = LRU(128)
+ _bbg_sql_insert: ClassVar[str]
+
+ def __init_subclass__(cls, deal_type, **kwargs):
+ super().__init_subclass__(deal_type, **kwargs)
+ if deal_type == DealType.Bond:
+ cls._bbg_sql_insert = (
+ f"INSERT INTO bond_tickets VALUES({','.join(['%s'] * 20)})"
+ )
+ elif deal_type == DealType.CDS:
+ cls._bbg_sql_insert = (
+ f"INSERT INTO cds_tickets VALUES({','.join(['%s'] * 22)})"
+ )
+ elif deal_type in (DealType.Fx, DealType.Spot, DealType.FxSwap):
+ cls._bbg_sql_insert = (
+ f"INSERT INTO fx_tickets VALUES({','.join(['%s'] * 211)})"
+ )
+
+ @classmethod
+ def commit(cls):
+ with cls._conn.cursor() as c:
+ try:
+ c.executemany(cls._bbg_sql_insert, cls._bbg_insert_queue)
+ except UniqueViolation as e:
+ logger.warning(e)
+ cls._conn.rollback()
+ else:
+ c.executemany(cls._sql_insert, cls._insert_queue)
+ cls._conn.commit()
+ finally:
+ cls._bbg_insert_queue.clear()
+ cls._insert_queue.clear()
+
+ @classmethod
+ def process(cls, file_handle, index):
+ for row in csv.DictReader(file_handle):
+ line = {"bbg_ticket_id": index, **row}
+ trade = cls.from_bbg_line(line)
+ trade.stage()
+ type(trade).commit()
+
+ @classmethod
+ def get_cp_code(cls, bbg_code, code_type):
+ with cls._conn.cursor() as c:
+ c.execute(
+ "SELECT cp_code from bbg_ticket_mapping where bbg_code=%s and code_type=%s",
+ (bbg_code, code_type),
+ )
+ try:
+ (cp_code,) = c.fetchone()
+ except TypeError:
+ raise ValueError(f"missing {bbg_code} in the db for {code_type}")
+ return cp_code
+
+
+class MTMDeal:
+ _mtm_queue: ClassVar[list] = []
+ _mtm_headers = None
+ _mtm_sftp = SftpClient.from_creds("mtm")
+ product_type: str
+
+ def __init_subclass__(cls, deal_type, **kwargs):
+ super().__init_subclass__(deal_type, **kwargs)
+ cls._mtm_headers = MTM_HEADERS[deal_type]
+ if deal_type == DealType.Swaption:
+ cls.product_type = "CDISW"
+ elif deal_type == DealType.CDS:
+ cls.product_type = "TRN"
+ elif deal_type == DealType.Termination:
+ cls.product_type = "TERM"
+ elif deal_type == DealType.TRS:
+ cls.product_type = "CDI"
+
+ @classmethod
+ def mtm_upload(cls):
+ if not cls._mtm_queue: # early exit
+ return
+ buf = StringIO()
+ csvwriter = csv.writer(buf)
+ csvwriter.writerow(cls._mtm_headers)
+ csvwriter.writerows(
+ [row.get(h, None) for h in cls._mtm_headers] for row in cls._mtm_queue
+ )
+ buf = buf.getvalue().encode()
+ fname = f"MTM.{datetime.datetime.now():%Y%m%d.%H%M%S}.{cls.product_type.capitalize()}.csv"
+ cls._mtm_sftp.put(buf, fname)
+ dest = DAILY_DIR / str(datetime.date.today()) / fname
+ dest.write_bytes(buf)
+ cls._mtm_queue.clear()
+
+ def mtm_stage(self):
+ self._mtm_queue.append(self.to_markit())
+
+ @classmethod
+ def from_dict(cls, **kwargs):
+ return cls(**{k: v for k, v in kwargs.items() if k in cls._sql_fields})
+
+
+class Citco:
+ _citco_headers = []
+ _citco_sftp = SftpClient.from_creds("citco")
+ _submission_queue = []
+
+ @classmethod
+ def citco_upload(cls):
+ if not cls._citco_queue: # early exit
+ return
+ buf = StringIO()
+ csvwriter = csv.writer(buf)
+ csvwriter.writerow(cls._citco_headers)
+ for h in cls._citco_queue:
+ _citco_to_action = {"R": "UPDATE", "D": "CANCEL", "N": "NEW"}
+ warnings.warn("we will get rid of overwriting")
+ h["Fund"] = "ISOSEL"
+ identifier = (
+ "instrument" if cls.file_tag == "i.innocap_serenitas." else "trade"
+ )
+ unique_id = (
+ h["UniqueIdentifier"]
+ if cls.file_tag == "i.innocap_serenitas."
+ else h["ClientOrderID"]
+ )
+ cls._submission_queue.append(
+ [
+ unique_id,
+ _citco_to_action[
+ h.get("OrdStatus", "N")
+ ], # We only update trades, not instruments
+ identifier,
+ ]
+ )
+ csvwriter.writerows(
+ [row.get(h, None) for h in cls._citco_headers] for row in cls._citco_queue
+ )
+ buf = buf.getvalue().encode()
+ cls._citco_sftp.client.chdir("/incoming")
+ cls._citco_sftp.put(buf, cls.fname())
+ cls.submission_commit()
+ dest = DAILY_DIR / str(datetime.date.today()) / cls.fname()
+ dest.write_bytes(buf)
+ cls._citco_queue.clear()
+ cls._submission_queue.clear()
+
+ def citco_stage(self, action="NEW"):
+ self._citco_queue.append(self.to_citco(action))
+
+ @classmethod
+ def fname(cls):
+ return f"{cls.file_tag}{datetime.datetime.now():%Y%m%d%H%M%S}.csv"
+
+ @classmethod
+ def submission_commit(cls):
+ sql_str = "INSERT INTO citco_submission_status (serenitas_id, action, identifier_type) VALUES (%s, %s, %s) "
+ with cls._conn.cursor() as c:
+ c.executemany(sql_str, cls._submission_queue)
+ cls._conn.commit()
+
+
+class CitcoProduct(Citco):
+ _citco_queue: ClassVar[list] = []
+ _citco_headers = GIL
+ product_key = ()
+ file_tag = "i.innocap_serenitas."
+
+ def __init_subclass__(cls, product_key, **kwargs):
+ cls.product_key = product_key
+
+ def get_productid(self):
+ filter_clause = " AND ".join([f"{k}=%s" for k in self.product_key])
+ sql_str = f"SELECT id, dealid, committed FROM {self._table_name} WHERE {filter_clause}"
+ with self._conn.cursor() as c:
+ c.execute(
+ sql_str,
+ tuple([getattr(self, k) for k in self.product_key]),
+ )
+ if results := c.fetchone():
+ (self.id, self.dealid, self.committed) = results
+
+ def to_citco(self, action):
+ obj = self.serialize("citco")
+ obj["Birth_date"] = obj["Birth_date"].strftime("%Y%m%d")
+ obj["Death_date"] = obj["Death_date"].strftime("%Y%m%d")
+ return obj
+
+
+class CitcoTrade(Citco):
+ _citco_queue: ClassVar[list] = []
+ _citco_headers = GTL
+ file_tag = "innocap_serenitas_trades_"
+
+ def to_citco(self, action):
+ obj = self.serialize("citco")
+ obj["SettleCurrency"] = "USD"
+ obj["OrdStatus"], obj["ExecTransType"] = self._action_to_citco(action)
+ obj["FillID"] = obj["ClientOrderID"]
+ obj["Trader"] = "DFLT"
+ obj["StrategyCode"] = f"{obj['portfolio']}/{obj['folder']}"
+ obj["TradeDate"] = (
+ obj["TradeDate"].strftime("%Y%m%d") if obj.get("TradeDate") else None
+ )
+ obj["SettlementDate"] = (
+ obj["SettlementDate"].strftime("%Y%m%d")
+ if obj.get("SettlementDate")
+ else None
+ )
+ obj["FillQty"] = obj.get("OrderQty")
+ obj["FillPrice"] = obj.get("AvgPrice")
+ obj["FXRate"] = 1
+ return obj
+
+ @staticmethod
+ def _action_to_citco(action):
+ match action:
+ case "NEW":
+ return ("N", 2)
+ case "UPDATE":
+ return ("R", 0)
+ case "CANCEL":
+ return ("D", 0)
+
+
+@dataclass
+class CDSDeal(
+ CitcoTrade,
+ BbgDeal,
+ MTMDeal,
+ Deal,
+ deal_type=DealType.CDS,
+ table_name="cds",
+ insert_ignore=("id", "dealid", "factor", "tenor", "redcode"),
+):
+ fund: Fund = field(metadata={"mtm": "Account Abbreviation", "citco": "Fund"})
+ account_code: str
+ cp_code: str = field(metadata={"mtm": "Broker Id", "citco": "ExecutionBroker"})
+ security_id: str = field(metadata={"mtm": "RED"})
+ security_desc: str = field(metadata={"citco": "SecurityDescription"})
+ maturity: datetime.date = field(metadata={"mtm": "Maturity Date"})
+ currency: Ccy = field(
+ metadata={"mtm": "Currency Code", "citco": "SecurityCurrency"}
+ )
+ protection: Literal["Buy", "Sell"]
+ notional: float = field(metadata={"mtm": "1st Leg Notional", "citco": "OrderQty"})
+ fixed_rate: float = field(metadata={"mtm": "1st Leg Rate"})
+ upfront: float = field(metadata={"mtm": "Initial Payment"})
+ traded_level: Decimal
+ effective_date: datetime.date = field(
+ default=None, metadata={"mtm": "Effective Date"}
+ )
+ portfolio: Portfolio = field(default=None)
+ folder: CdsStrat = field(default=None)
+ payment_rolldate: BusDayConvention = BusDayConvention.following
+ day_count: DayCount = "ACT/360"
+ frequency: Frequency = Frequency.Quarterly
+ trade_date: datetime.date = field(
+ default_factory=datetime.date.today(),
+ metadata={"mtm": "Trade Date", "citco": "TradeDate"},
+ )
+ upfront_settle_date: datetime.date = field(
+ default_factory=lambda: next_business_day(datetime.date.today()),
+ metadata={"mtm": "First Payment Date", "citco": "SettlementDate"},
+ )
+ orig_attach: int = field(default=None, metadata={"mtm": "Attachment Point"})
+ orig_detach: int = field(default=None, metadata={"mtm": "Exhaustion Point"})
+ tenor: int = field(init=False, metadata={"insert": False})
+ attach: int = field(default=None)
+ detach: int = field(default=None)
+ swap_type: SwapType = "CD_INDEX"
+ clearing_facility: ClearingFacility = "ICE-CREDIT"
+ isda_definition: IsdaDoc = "ISDA2014"
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None,
+ metadata={"insert": False, "mtm": "Swap ID", "citco": "ClientOrderID"},
+ )
+ initial_margin_percentage: float = field(
+ default=None, metadata={"mtm": "Independent Amount (%)"}
+ )
+ factor: float = field(default=1.0, init=False, metadata={"insert": False})
+ redcode: str = field(init=False, metadata={"insert": False})
+ bbg_ticket_id: str = None
+
+ def __post_init__(self):
+ start_protection = self.trade_date + datetime.timedelta(days=1)
+ effective_date = previous_twentieth(prev_business_day(start_protection))
+ self.effective_date = adjust_next_business_day(effective_date)
+ if self.attach:
+ self.factor = (self.detach - self.attach) / (
+ self.orig_detach - self.orig_attach
+ )
+ else:
+ with self._conn.cursor() as c:
+ c.execute(
+ "SELECT indexfactor / 100 FROM index_version WHERE redindexcode=%s",
+ (self.security_id,),
+ )
+ (self.factor,) = c.fetchone()
+ # do something better
+ self.tenor = self.security_desc.rsplit(" ", 1)[1].removesuffix("Y")
+ self.redcode = "_".join((self.security_id, self.tenor))
+
+ def to_markit(self):
+ obj = self.serialize("mtm")
+ if obj["Initial Payment"] >= 0:
+ obj["Transaction Code"] = "Receive"
+ else:
+ obj["Transaction Code"] = "Pay"
+ obj["Initial Payment"] = round(abs(obj["Initial Payment"]), 2)
+ obj["Trade ID"] = obj["Swap ID"]
+ obj["Product Type"] = "TRN"
+ obj["Transaction Type"] = "NEW"
+ obj["Protection"] = "Buy" if obj["protection"] == "Buyer" else "Sell"
+ obj["Entity Matrix"] = "Publisher"
+ obj["Definitions Type"] = "ISDA2014Credit"
+ # obj["Independent Amount (%)"] = obj["initial_margin_percentage"]
+ if "ITRX" in obj["security_desc"]:
+ obj["Include Contractual Supplement"] = "Y"
+ obj["Contractual Supplement"] = "StandardiTraxxEuropeTranche"
+ return obj
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ obj["SecurityType"] = "CDS"
+ obj["AvgPrice"] = (
+ obj["OrderQty"] / obj["upfront"] / obj["factor"] / 100
+ ) # Citco looks at factor as 1/100
+ if obj["protection"] == "Buyer":
+ obj["BuySellShortCover"] = "S"
+ else:
+ obj["BuySellShortCover"] = "B"
+ obj["AvgPrice"] = -obj["AvgPrice"]
+ obj["FillPrice"] = obj["AvgPrice"]
+ if obj["orig_attach"] is not None:
+ # tranche process
+ obj["IDSource"] = "USERID"
+ obj["ExecutionBroker"] = _citco_cp_isda[obj["ExecutionBroker"]]
+ obj["ClearingAgent"] = obj["ExecutionBroker"]
+ obj["SecurityID"] = self.product.dealid
+ else:
+ # cleared cds process
+ obj["IDSource"] = "RED"
+ obj["ExecutionBroker"] = (
+ _citco_cp_cdea[obj["ExecutionBroker"]]
+ if obj["ExecutionBroker"] != "BSEONY"
+ else "BSEONY"
+ )
+ # We need to query DB via accounts table here
+ warnings.warn("we will get rid of overwriting")
+ obj["ClearingAgent"] = "BOA_FC"
+ obj["SecurityID"] = self.redcode
+
+ return obj
+
+ @property
+ def product(self):
+ return TrancheProduct(
+ underlying_security_id=self.redcode,
+ attach=self.orig_attach,
+ detach=self.orig_detach,
+ death_date=self.maturity,
+ security_desc=f"{self.security_desc} {self.orig_attach}-{self.orig_detach}",
+ )
+
+ @classmethod
+ def from_bbg_line(cls, line: dict):
+ if line["Client FCM"] == "":
+ raise ValueError("Trade is unallocated")
+ if line["Coupon"] == "":
+ with cls._conn.cursor() as c:
+ c.execute(
+ "SELECT coupon, index, series, tenor FROM index_desc "
+ "WHERE redindexcode=%s AND maturity =%s",
+ (
+ line["Red Code"],
+ datetime.datetime.strptime(line["Mat Dt"], "%m/%d/%Y").date(),
+ ),
+ )
+ coupon, index, series, tenor = c.fetchone()
+ line["Security"] = desc_str(index, series, tenor.removesuffix("yr"))
+ line["Coupon"] = coupon
+ if "Price (Dec)" not in line: # Means this is a BSEF block file
+ line["Price (Dec)"] = line["Price"]
+ line["Quantity"] = float(line["Qty (M)"]) * 1000
+ values = [line["bbg_ticket_id"]] + [None] * 21
+ values[14] = _funds[_fcms[line["Client FCM"]]]
+ values[15] = _fcms[line["Client FCM"]]
+ else:
+ values = list(line.values())
+ cp_code = cls.get_cp_code(line["Brkr"], "CDS")
+ cls._bbg_insert_queue.append(values)
+ return cls(
+ fund=_funds[_fcms[line["Client FCM"]]],
+ folder="*",
+ portfolio="UNALLOCATED",
+ security_id=line["Red Code"],
+ security_desc=line["Security"].removesuffix(" PRC"),
+ traded_level=Decimal(line["Price (Dec)"]),
+ notional=line["Quantity"],
+ fixed_rate=float(line["Coupon"]) * 0.01,
+ trade_date=datetime.datetime.strptime(line["Trade Dt"], "%m/%d/%Y").date(),
+ maturity=datetime.datetime.strptime(line["Mat Dt"], "%m/%d/%Y").date(),
+ currency=line["Curncy"],
+ protection="Buyer" if line["Side"] == "B" else "Seller",
+ upfront=line["Net"],
+ cp_code=cp_code,
+ account_code=_fcms[line["Client FCM"]],
+ bbg_ticket_id=line["bbg_ticket_id"],
+ )
+
+
+@dataclass
+class BondDeal(
+ CitcoTrade,
+ BbgDeal,
+ Deal,
+ deal_type=DealType.Bond,
+ table_name="bonds",
+ insert_ignore=("id", "dealid"),
+):
+ buysell: bool
+ description: str
+ faceamount: float = field(metadata={"citco": "OrderQty"})
+ price: float = field(metadata={"citco": "AvgPrice"})
+ cp_code: str = field(metadata={"citco": "ExecutionBroker"})
+ cusip: str = None
+ isin: str = None
+ identifier: str = field(default=None, metadata={"citco": "SecurityID"})
+ trade_date: datetime.date = field(
+ default_factory=datetime.date.today(), metadata={"citco": "TradeDate"}
+ )
+ settle_date: datetime.date = field(
+ default_factory=lambda: next_business_day(datetime.date.today()),
+ metadata={"citco": "SettlementDate"},
+ )
+ folder: BondStrat = field(default=None)
+ portfolio: Portfolio = field(default=None)
+ asset_class: AssetClass = field(default=None)
+ bbg_ticket_id: str = None
+ principal_payment: float = None
+ accrued_payment: float = None
+ current_face: float = None
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None,
+ metadata={"insert": False, "mtm": "Swap ID", "citco": "ClientOrderID"},
+ )
+
+ @classmethod
+ def from_bbg_line(cls, line: dict):
+ with cls._conn.cursor() as c:
+ c.execute(
+ "SELECT asset_class from securities where figi=%s",
+ (line["FIGI"],),
+ )
+ results = c.fetchone()
+ asset_class = results[0] if results else None
+ cp_code = cls.get_cp_code(line["Brkr"], "BOND")
+ cls._bbg_insert_queue.append(list(line.values()))
+ return cls(
+ faceamount=Decimal(line["Quantity"]),
+ price=Decimal(line["Price (Dec)"]),
+ cp_code=cp_code,
+ cusip=line["Cusip"],
+ identifier=line["Cusip"],
+ trade_date=datetime.datetime.strptime(line["Trade Dt"], "%m/%d/%Y"),
+ settle_date=datetime.datetime.strptime(line["SetDt"], "%m/%d/%Y"),
+ portfolio="UNALLOCATED",
+ description=line["Security"].removesuffix(" Mtge"),
+ buysell=line["Side"] == "B",
+ bbg_ticket_id=line["bbg_ticket_id"],
+ asset_class=asset_class,
+ )
+
+ @classmethod
+ def from_allocationid(cls, allocation_id):
+ with cls._conn.cursor() as c:
+ c.execute(
+ "SELECT tradeid, notional from bond_allocation where id=%s",
+ (allocation_id,),
+ )
+ tradeid, notional = c.fetchone()
+ cls = cls.from_tradeid(tradeid)
+ ratio = notional / cls.faceamount
+ for key in [
+ "principal_payment",
+ "accrued_payment",
+ "current_face",
+ "net_amount",
+ ]:
+ if key in cls.__dict__.keys():
+ setattr(cls, key, getattr(cls, key) * ratio)
+ setattr(cls, "faceamount", notional)
+ return cls
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ obj["SecurityType"] = "CMO"
+ warnings.warn("Hardcoded")
+ obj["ClearingAgent"] = "NT"
+ obj["FXRate"] = 1
+ obj["BuySellShortCover"] = "B" if obj["buysell"] else "S"
+ obj["IDSource"] = "CUSIP"
+ with self._conn.cursor() as c:
+ c.execute(
+ "SELECT coupon, day_count from securities where identifier=%s",
+ (obj["SecurityID"],),
+ )
+ obj["Coupon%"], obj["DayCountFraction/RepoCalendar"] = c.fetchone()
+ return obj
+
+
+@dataclass
+class SwaptionDeal(
+ CitcoTrade,
+ MTMDeal,
+ Deal,
+ deal_type=DealType.Swaption,
+ table_name="swaptions",
+ insert_ignore=("id", "dealid", "factor"),
+):
+ buysell: bool
+ fund: Fund = field(metadata={"mtm": "Account Abbreviation", "citco": "Fund"})
+ cp_code: str = field(metadata={"mtm": "Broker Id", "citco": "ExecutionBroker"})
+ security_id: str = field(metadata={"mtm": "RED"})
+ security_desc: str = field(metadata={"citco": "SecurityDescription"})
+ maturity: datetime.date = field(metadata={"mtm": "Maturity Date"})
+ currency: Ccy = field(
+ metadata={"mtm": "Currency Code", "citco": "SecurityCurrency"}
+ )
+ notional: float = field(metadata={"mtm": "1st Leg Notional", "citco": "OrderQty"})
+ fixed_rate: float = field(metadata={"mtm": "1st Leg Rate"})
+ strike: float = field(metadata={"mtm": "Strike Price"})
+ price: float = field(metadata={"citco": "AvgPrice"})
+ option_type: OptionType
+ expiration_date: datetime.date = field(metadata={"mtm": "Expiration"})
+ portfolio: Portfolio = field(default=None)
+ folder: SwaptionStrat = field(default=None)
+ trade_date: datetime.date = field(
+ default_factory=datetime.date.today(),
+ metadata={"mtm": "Trade Date", "citco": "TradeDate"},
+ )
+ settle_date: datetime.date = field(
+ default_factory=lambda: next_business_day(datetime.date.today()),
+ metadata={"mtm": "Settle Date", "citco": "SettlementDate"},
+ )
+ expiration_date: datetime.date = field(
+ metadata={"mtm": "Swaption Expiration Date"},
+ )
+ initial_margin_percentage: float = field(
+ default=None, metadata={"mtm": "Independent Amount (%)"}
+ )
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None,
+ metadata={"insert": False, "mtm": "Swap ID", "citco": "ClientOrderID"},
+ )
+ factor: float = field(default=1.0, init=False, metadata={"insert": False})
+
+ def __post_init__(self):
+ # will need to filter a bit better, for now, just CDX index swaptions
+ if self.security_desc:
+ with self._conn.cursor() as c:
+ c.execute(
+ "SELECT indexfactor / 100 FROM index_version WHERE redindexcode=%s",
+ (self.security_id,),
+ )
+ (self.factor,) = c.fetchone()
+ self.tenor = self.security_desc.rsplit(" ", 1)[1].removesuffix("Y")
+ self.redcode = "_".join((self.security_id, self.tenor))
+
+ def to_markit(self):
+ obj = self.serialize("mtm")
+ obj["Initial Payment"] = (
+ round(obj["price"] * obj["1st Leg Notional"] * 0.01, 2) * self.factor
+ )
+ obj["Trade ID"] = obj["Swap ID"]
+ obj["Product Type"] = self.product_type
+ obj["Transaction Type"] = "NEW"
+ if obj["buysell"]:
+ obj["Transaction Code"] = "Pay"
+ obj["Protection"] = "Buy" if obj["option_type"] == "PAYER" else "Sell"
+ obj["OptionBuySellIndicator"] = "Buy"
+ else:
+ obj["Transaction Code"] = "Receive"
+ obj["Protection"] = "Sell" if obj["option_type"] == "PAYER" else "Buy"
+ obj["OptionBuySellIndicator"] = "Sell"
+ obj["Entity Matrix"] = "Publisher"
+ obj["Clearing House"] = "ICE_FCM_US"
+ obj["Swaption Settlement Type"] = "Physical"
+ obj["Supplement Date"] = datetime.date(2021, 12, 13)
+ obj["Supplement 2 Date"] = datetime.date(2020, 1, 27)
+ if "IG" in obj["security_desc"]:
+ obj["Swaption Quotation Rate Type"] = "Spread"
+ obj["Strike Price"] = obj["Strike Price"] * 0.01
+ obj["Effective Date"] = obj["Trade Date"]
+ return obj
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ obj["ExecutionBroker"] = _citco_cp_isda[obj["ExecutionBroker"]]
+ obj["ClearingAgent"] = obj["ExecutionBroker"]
+ obj["SecurityType"] = "BNDOPT"
+ obj["BuySellShortCover"] = "B" if obj["buysell"] == "Buy" else "S"
+ obj["IDSource"] = "USERID"
+ obj["ClearingAgent"] = obj["ExecutionBroker"]
+ obj["SecurityID"] = self.product.dealid
+ return obj
+
+ @property
+ def product(self):
+ return SwaptionProduct(
+ underlying_security_id=self.redcode,
+ instrument_type="BNDO",
+ callput=self.option_type == "RECEIVER",
+ strike=self.strike,
+ expiration=self.expiration_date,
+ )
+
+
+@dataclass
+class TerminationDeal(
+ MTMDeal,
+ Deal,
+ deal_type=DealType.Termination,
+ table_name="terminations",
+ insert_ignore=("id", "dealid", "orig_cp", "currency", "fund", "product_type"),
+):
+ partial_termination: bool
+ termination_fee: float = field(metadata={"mtm": "Initial Payment"})
+ fee_payment_date: datetime.date = field(
+ metadata={"mtm": "Settle Date", "globeop": "FeePaymentDate"}
+ )
+ termination_cp: str = field(metadata={"mtm": "Broker Id"})
+ termination_amount: float = field(
+ metadata={"mtm": "1st Leg Notional", "globeop": "TerminationAmount"}
+ )
+ termination_date: datetime.date = field(
+ default_factory=datetime.date.today(),
+ metadata={"mtm": "Trade Date", "globeop": "TerminationDate"},
+ )
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(default=None, metadata={"insert": False, "mtm": "Swap ID"})
+ factor: float = field(default=1, init=False, metadata={"insert": False})
+ orig_cp: str = field(
+ init=False,
+ metadata={"mtm": "Remaining Party", "insert": False},
+ )
+ currency: str = field(
+ init=False,
+ metadata={"mtm": "Currency Code", "insert": False},
+ )
+ fund: str = field(
+ init=False,
+ metadata={"mtm": "Account Abbreviation", "insert": False},
+ )
+ product_type: str = field(
+ init=False, metadata={"mtm": "Product Type", "insert": False}
+ )
+ deal_type: str = field(
+ init=False, metadata={"insert": False, "globeop": "DealType"}
+ )
+ globeop_id: str = field(init=False, default=None, metadata={"globeop": "GoTradeId"})
+
+ def __post_init__(self):
+ if self.dealid.startswith("SWPTN"):
+ self.product_type = "CDISW"
+ self.deal_type = "SwaptionDeal"
+ sql_str = (
+ "SELECT cp_code, currency, fund, globeop_id FROM terminations "
+ "LEFT JOIN swaptions USING (dealid) "
+ "WHERE terminations.id = %s"
+ )
+ elif self.dealid.startswith("SCCDS"):
+ self.product_type = "TRN"
+ self.deal_type = "CreditDefaultSwapDeal"
+ sql_str = (
+ "SELECT cp_code, currency, fund, b.globeop_id, "
+ "(detach - attach) / (orig_detach - orig_attach) "
+ "FROM terminations "
+ "LEFT JOIN cds USING (dealid) "
+ "LEFT JOIN LATERAL ("
+ " SELECT globeop_id FROM id_mapping WHERE serenitas_id=cds.id"
+ " ORDER BY date DESC LIMIT 1"
+ ") b ON true "
+ "WHERE terminations.id = %s"
+ )
+ with self._conn.cursor() as c:
+ c.execute(sql_str, (self.id,))
+ if self.deal_type == "SwaptionDeal":
+ self.orig_cp, self.currency, self.fund, self.globeop_id = c.fetchone()
+ elif self.deal_type == "CreditDefaultSwapDeal":
+ (
+ self.orig_cp,
+ self.currency,
+ self.fund,
+ self.globeop_id,
+ self.factor,
+ ) = c.fetchone()
+
+ def to_markit(self):
+ obj = self.serialize("mtm")
+ if obj["Initial Payment"] >= 0:
+ obj["Transaction Code"] = "Receive"
+ else:
+ obj["Transaction Code"] = "Pay"
+ obj["Initial Payment"] = round(abs(obj["Initial Payment"]), 2)
+ obj["Trade ID"] = obj["Swap ID"] + "-" + str(obj["id"])
+ obj["Transaction Type"] = (
+ "Termination"
+ if obj["Remaining Party"] == obj["Broker Id"]
+ else "Assignment"
+ )
+ obj["Effective Date"] = obj["Trade Date"] + datetime.timedelta(days=1)
+ obj["Product Type"] = obj["product_type"]
+ return obj
+
+ def to_globeop(self):
+ obj = self.serialize("globeop")
+ obj["TerminationAmount"] *= self.factor
+ obj["FeesPaid"] = (
+ -obj["termination_fee"] if obj["termination_fee"] < 0 else None
+ )
+ obj["FeesReceived"] = (
+ obj["termination_fee"] if obj["termination_fee"] > 0 else None
+ )
+ obj["Action"] = "UPDATE"
+ obj["Client"] = _client_name[obj["fund"]]
+ obj["SubAction"] = "Termination"
+ if self.termination_cp != self.orig_cp:
+ obj["AssignedCounterparty"] = self.termination_cp
+ obj["PartialTermination"] = "Y" if self.partial_termination else "N"
+ return obj
+
+
+@dataclass
+class SpotDeal(
+ CitcoTrade,
+ BbgDeal,
+ Deal,
+ deal_type=DealType.Spot,
+ table_name="spots",
+ insert_ignore=("id", "dealid"),
+):
+ folder: SpotStrat
+ portfolio: Portfolio
+ spot_rate: float = field(metadata={"citco": "AvgPrice"})
+ buy_currency: str
+ buy_amount: float
+ sell_currency: str
+ sell_amount: float
+ fund: Fund
+ cp_code: str = field(metadata={"citco": "ExecutionBroker"})
+ cash_account: str
+ commission_currency: str = "USD"
+ commission: float = None
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "ClientOrderID"}
+ )
+ trade_date: datetime.date = field(
+ default_factory=datetime.date.today(), metadata={"citco": "TradeDate"}
+ )
+ settle_date: datetime.date = field(
+ default_factory=datetime.date.today(), metadata={"citco": "SettlementDate"}
+ )
+ bbg_ticket_id: str = None
+
+ @classmethod
+ def from_bbg_line(cls, line: dict):
+ cp_code = cls.get_cp_code(line["Counterparty Deal Code"], "FX")
+ if line["Side"] == "B":
+ key1, key2 = "buy", "sell"
+ else:
+ key1, key2 = "sell", "buy"
+
+ d = {
+ f"{key1}_currency": line["Currency 1"],
+ f"{key1}_amount": line["Amount Dealt"],
+ f"{key2}_currency": line["Currency 2"],
+ f"{key2}_amount": line["Counter Amount"],
+ }
+ for key in ("Comp Quote 1",):
+ if line[key] == "":
+ line[key] = None
+ cls._bbg_insert_queue.append(list(line.values()))
+ return cls(
+ folder="*",
+ portfolio="UNALLOCATED",
+ cp_code=cp_code,
+ trade_date=datetime.datetime.strptime(line["Date Of Deal"], "%Y%m%d"),
+ settle_date=datetime.datetime.strptime(
+ line["Value Date Period 1 Currency 1"], "%Y%m%d"
+ ),
+ fund=_fx_funds[line["ALOC Account 1"]],
+ spot_rate=line["Exchange Rate Period 1"],
+ cash_account=_fx_accounts[line["ALOC Account 1"]],
+ bbg_ticket_id=line["bbg_ticket_id"],
+ **d,
+ )
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ if obj["buy_currency"] == "USD":
+ key1, key2 = "sell", "buy"
+ else:
+ key1, key2 = "buy", "sell"
+ obj["SecurityCurrency"] = obj[f"{key1}_currency"]
+ obj["OrderQty"] = obj[f"{key1}_amount"]
+ obj["FillQty"] = obj["OrderQty"]
+ obj["SecurityType"] = "FX"
+ obj["BuySellShortCover"] = "S" if obj["buy_currency"] == "USD" else "B"
+ obj["IDSource"] = "BLOOMBERG"
+ _citco_currency_mapping = {"EUR": "EURUSD CURNCY"}
+ obj["SecurityID"] = _citco_currency_mapping[obj["SecurityCurrency"]]
+ obj["ClearingAgent"] = "NT"
+ obj["FillFXSettleAmount"] = obj[f"{key2}_amount"]
+ obj["FXRate"] = 1
+ return obj
+
+
+_fx_funds = {"serenitas": "SERCGMAST", "bowdst": "BOWDST", "baml_fcm": "SERCGMAST"}
+_fx_accounts = {"serenitas": "V0NSCLMAMB", "bowdst": "751254", "baml_fcm": "V0NSCLMSPT"}
+
+
+class FxDeal(BbgDeal, Deal, table_name=None, deal_type=DealType.Fx):
+ @classmethod
+ def from_bbg_line(cls, line: dict):
+ if line["Deal Type"] in ("4", "2"):
+ return SpotDeal.from_bbg_line(line)
+ else:
+ return FxSwapDeal.from_bbg_line(line)
+
+
+@dataclass
+class FxSwapDeal(
+ CitcoTrade,
+ BbgDeal,
+ Deal,
+ deal_type=DealType.FxSwap,
+ table_name="fx_swaps",
+ insert_ignore=("id", "dealid"),
+):
+ folder: str
+ portfolio: str
+ trade_date: datetime.date = field(metadata={"citco": "TradeDate"})
+ near_settle_date: datetime.date
+ near_buy_currency: str
+ near_buy_amount: float
+ near_sell_currency: str
+ near_sell_amount: float
+ near_rate: float
+ far_rate: float
+ far_settle_date: datetime.date
+ far_buy_currency: str
+ far_buy_amount: float
+ far_sell_currency: str
+ far_sell_amount: str
+ fund: Fund
+ cp_code: str
+ cash_account: str
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "ClientOrderID"}
+ )
+ bbg_ticket_id: str = None
+
+ @classmethod
+ def from_bbg_line(cls, line: dict):
+ cp_code = cls.get_cp_code(line["Counterparty Deal Code"], "FX")
+ if line["Side"] == "S":
+ key1, key2 = "buy", "sell"
+ else:
+ key1, key2 = "sell", "buy"
+
+ d = {
+ f"near_{key1}_currency": line["Currency 1"],
+ f"near_{key1}_amount": line["Amount Dealt"],
+ f"far_{key1}_currency": line["Currency 2"],
+ f"far_{key1}_amount": line["Far Counter Amount"],
+ f"near_{key2}_currency": line["Currency 2"],
+ f"near_{key2}_amount": line["Counter Amount"],
+ f"far_{key2}_currency": line["Currency 1"],
+ f"far_{key2}_amount": line["Far Amount Dealt"],
+ }
+ for key in ("Comp Quote 1",):
+ if line[key] == "":
+ line[key] = None
+ cls._bbg_insert_queue.append(list(line.values()))
+ return cls(
+ folder="*",
+ portfolio="UNALLOCATED",
+ cp_code=cp_code,
+ trade_date=datetime.datetime.strptime(line["Date Of Deal"], "%Y%m%d"),
+ near_settle_date=datetime.datetime.strptime(
+ line["Value Date Period 1 Currency 1"], "%Y%m%d"
+ ),
+ far_settle_date=datetime.datetime.strptime(
+ line["Value Date Period 2 Currency 1"], "%Y%m%d"
+ ),
+ fund=_fx_funds[line["ALOC Account 1"]],
+ near_rate=line["Exchange Rate Period 1"],
+ far_rate=line["Exchange Rate Period 2"],
+ cash_account=_fx_accounts[line["ALOC Account 1"]],
+ bbg_ticket_id=line["bbg_ticket_id"],
+ **d,
+ )
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ if obj["near_buy_currency"] == "USD": # This is for strict FX Swaps
+ key1, key2 = "buy", "sell"
+ else:
+ key1, key2 = "sell", "buy"
+ obj["SecurityCurrency"] = obj[f"far_{key1}_currency"]
+ obj["OrderQty"] = obj[f"far_{key1}_amount"]
+ obj["SecurityType"] = "FWDFX"
+ obj["AvgPrice"] = obj["far_rate"]
+ obj["BuySellShortCover"] = "B" if obj["near_buy_currency"] == "USD" else "S"
+ obj["IDSource"] = "BLOOMBERG"
+ _citco_currency_mapping = {"EUR": "EURUSD CURNCY"}
+ obj["SecurityID"] = _citco_currency_mapping[obj["SecurityCurrency"]]
+ obj["ClearingAgent"] = "NT"
+ obj["SettlementDate"] = obj["far_settle_date"]
+ obj["FillFXSettleAmount"] = obj[f"far_{key2}_amount"]
+ near_trade = SpotDeal(
+ folder=obj["folder"],
+ portfolio=obj["portfolio"],
+ spot_rate=obj["near_rate"],
+ buy_currency=obj[f"near_{key1}_currency"],
+ buy_amount=obj[f"near_{key1}_amount"],
+ sell_currency=obj[f"near_{key2}_currency"],
+ sell_amount=obj[f"near_{key2}_amount"],
+ fund=obj["fund"],
+ dealid=obj["ClientOrderID"] + "_N",
+ trade_date=datetime.datetime.strptime(
+ obj["TradeDate"], "%Y%m%d"
+ ), # Will be cleaning up with a split function, this is just to run it
+ settle_date=obj["near_settle_date"],
+ cp_code=obj["cp_code"],
+ cash_account=obj["cash_account"],
+ )
+ obj["ClientOrderID"] = obj["ClientOrderID"] + "_F"
+ obj["FXRate"] = 1
+ return obj
+
+
+@dataclass
+class TRSDeal(
+ CitcoTrade,
+ MTMDeal,
+ Deal,
+ deal_type=DealType.TRS,
+ table_name="trs",
+ insert_ignore=("id", "dealid", "orig_cp", "currency", "product_type"),
+):
+ fund: str = field(
+ metadata={"mtm": "Account Abbreviation", "globeop": "Fund"},
+ )
+ portfolio: str = field(metadata={"globeop": "Portfolio"})
+ folder: str = field(metadata={"globeop": "Folder"})
+ cash_account: str = field(metadata={"globeop": "Cash Account"})
+ cp_code: str = field(
+ metadata={
+ "globeop": "Counterparty",
+ "mtm": "Broker Id",
+ "citco": "ExecutionBroker",
+ }
+ )
+ trade_date: datetime.date = field(
+ metadata={"globeop": "Trade Date", "mtm": "Trade Date", "citco": "TradeDate"}
+ )
+ effective_date: datetime.date = field(
+ init=False, metadata={"mtm": "Effective Date", "citco": "SettlementDate"}
+ )
+ maturity_date: datetime.date = field(metadata={"mtm": "Maturity Date"})
+ funding_index: str
+ buysell: bool
+ underlying_security: str
+ price: float = field(metadata={"mtm": "Initial Fixing Amount", "citco": "AvgPrice"})
+ accrued: float = field(metadata={"mtm": "Initial Payment", "citco": "Fee"})
+ funding_freq: str
+ funding_daycount: str
+ funding_payment_roll_convention: str
+ funding_arrears: bool
+ asset_freq: str
+ asset_daycount: str
+ asset_payment_roll_convention: str
+ initial_margin_percentage: float = field(
+ metadata={"globeop": "InitialMarginPercent", "mtm": "Independent Amount (%)"}
+ )
+ notional: float = field(metadata={"mtm": "1st Leg Notional", "citco": "OrderQty"})
+ currency: str = field(
+ metadata={"mtm": "Currency Code", "citco": "SecurityCurrency"}
+ )
+ interest_calc_method: str
+ compound_avg_frequency: str
+ fixing_frequency: str
+ cpty_id: str
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None,
+ metadata={
+ "insert": False,
+ "mtm": "Swap ID",
+ "globeop": "Deal Id",
+ "citco": "ClientOrderID",
+ },
+ )
+
+ def __post_init__(self):
+ self.effective_date = self.trade_date + datetime.timedelta(days=1)
+
+ def to_markit(self):
+ _trs_red = {"IBOXHY": "4J623JAA8"}
+ _mtm_index = {"SOFRRATE": "USD-SOFR-COMPOUND"}
+ obj = self.serialize("mtm")
+ obj["Trade ID"] = obj["Swap ID"]
+ obj["Initial Payment Currency"] = obj["Currency Code"]
+ if obj["Initial Payment"] >= 0:
+ obj["Transaction Code"] = "Receive"
+ else:
+ obj["Transaction Code"] = "Pay"
+ obj["Initial Payment"] = round(abs(obj["Initial Payment"]), 2)
+ obj["Product Sub Type"] = "IBOXX" # Hardcoded for now
+ obj["RED"] = _trs_red[obj["underlying_security"]]
+ obj["Transaction Type"] = "New"
+ obj["Protection"] = "Buy" if obj["buysell"] else "Sell"
+ obj["Master Document Date"] = datetime.date(2020, 12, 18)
+ obj["Supplement Date"] = datetime.date(2015, 2, 18)
+ obj["Product Type"] = self.product_type
+ obj["Independent Amount Payer"] = obj["Account Abbreviation"]
+ obj["2nd Leg Index"] = _mtm_index[obj["funding_index"]]
+ obj["2nd Leg Spread"] = 0
+ obj["2nd Leg Initial Floating Rate"] = 0
+ return obj
+
+ def to_globeop(self):
+ obj = self.serialize("globeop")
+ if obj["buysell"]:
+ key1, key2 = "Pay", "Receive"
+ else:
+ key1, key2 = "Receive", "Pay"
+ d = {
+ f"{key1}LegRateType": "Floating",
+ f"{key1}UnderlyingType": "Interest",
+ f"{key1}FloatRate": obj["funding_index"],
+ f"{key1}FixedRate": 0,
+ f"{key1}Daycount": obj["funding_daycount"],
+ f"{key1}Frequency": obj["funding_freq"],
+ f"{key1}EffectiveDate": obj["effective_date"],
+ f"{key1}MaturityDate": obj["maturity_date"],
+ f"{key1}Notional": obj["notional"],
+ f"{key1}PaymentBDC": obj["funding_payment_roll_convention"],
+ f"{key1}Arrears": "Y" if obj["funding_arrears"] else "N",
+ f"{key1}InterestCalcMethod": obj["interest_calc_method"],
+ f"{key1}CompoundAverageFrequency": obj["compound_avg_frequency"],
+ f"{key1}Currency": obj["currency"],
+ f"{key1}FixingFrequency": obj["fixing_frequency"],
+ f"{key2}LegRateType": "Fixed",
+ f"{key2}UnderlyingType": "Bond",
+ f"{key2}UnderlyingSecurity": obj["underlying_security"],
+ f"{key2}Daycount": obj["asset_daycount"],
+ f"{key2}Frequency": obj["asset_freq"],
+ f"{key2}EffectiveDate": obj["effective_date"],
+ f"{key2}MaturityDate": obj["maturity_date"],
+ f"{key2}Notional": obj["notional"],
+ f"{key2}PaymentBDC": obj["asset_payment_roll_convention"],
+ f"{key2}Price": obj["price"],
+ f"{key2}Currency": obj["currency"],
+ }
+ obj["SwapType"] = "TOTAL_RETURN_SWAP"
+ obj["Deal Type"] = "TotalReturnSwapDeal"
+ obj["Action"] = "NEW" # Need to figure this out
+ obj["Client"] = "Serenitas"
+ obj["State"] = "Valid"
+ obj["Custodian"] = "BAC"
+ obj.update(d)
+ return obj
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ obj["ExecutionBroker"] = _citco_cp_isda[obj["ExecutionBroker"]]
+ obj["ClearingAgent"] = obj["ExecutionBroker"]
+ obj["SecurityType"] = "TRS"
+ obj["BuySellShortCover"] = "B" if obj["buysell"] else "S"
+ obj["IDSource"] = "USERID"
+ obj["Fee"] = -obj["Fee"] if obj["buysell"] else obj["Fee"]
+ obj["SecurityID"] = self.product.dealid
+ return obj
+
+ @property
+ def product(self):
+ return TRSProduct(
+ birth_date=self.trade_date,
+ death_date=self.maturity_date,
+ underlying_security=self.underlying_security,
+ funding_index=self.funding_index,
+ )
+
+
+@dataclass
+class IRSDeal(
+ CitcoTrade,
+ Deal,
+ deal_type=DealType.IRS,
+ table_name="irs",
+ insert_ignore=("id", "dealid", "orig_cp", "product_type"),
+):
+ fund: str = field(
+ metadata={"mtm": "Account Abbreviation", "globeop": "Fund"},
+ )
+ portfolio: str = field(metadata={"globeop": "Portfolio"})
+ folder: str = field(metadata={"globeop": "Folder"})
+ cash_account: str = field(metadata={"globeop": "Cash Account"})
+ cp_code: str = field(
+ metadata={"globeop": "GiveUpCounterparty", "citco": "ExecutionBroker"}
+ )
+ trade_date: datetime.date = field(
+ metadata={"globeop": "Trade Date", "citco": "TradeDate"}
+ )
+ effective_date: datetime.date = field(metadata={"citco": "SettlementDate"})
+ maturity_date: datetime.date
+ payreceive: bool
+ fixed_rate: float = field(metadata={"citco": "AvgPrice"})
+ fixed_daycount: str
+ fixed_payment_freq: str
+ fixed_bdc: str
+ notional: float = field(metadata={"citco": "OrderQty"})
+ float_index: str
+ float_daycount: str
+ float_payment_freq: str
+ float_bdc: str
+ float_arrears: bool
+ float_fixing_freq: str
+ pay_interest_calc_method: str
+ clearing_facility: str = field(metadata={"globeop": "ClearingFacility"})
+ swap_type: str = field(metadata={"globeop": "SwapType"})
+ cleared_trade_id: str
+ currency: str = field(metadata={"citco": "SecurityCurrency"})
+ custodian: int = field(
+ default=None, metadata={"insert": False, "globeop": "Custodian"}, init=False
+ )
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None,
+ metadata={
+ "insert": False,
+ "mtm": "Swap ID",
+ "globeop": "Deal Id",
+ "citco": "ClientOrderID",
+ },
+ )
+
+ def __post_init__(self):
+ with self._conn.cursor() as c:
+ c.execute(
+ "SELECT cp_code from accounts2 where cash_account=%s",
+ (self.cash_account,),
+ )
+ (self.custodian,) = c.fetchone()
+
+ def to_globeop(self):
+ obj = self.serialize("globeop")
+ if obj["payreceive"]:
+ key1, key2 = "Receive", "Pay"
+ else:
+ key1, key2 = "Pay", "Receive"
+ d = {
+ f"{key1}LegRateType": "Float",
+ f"{key1}FloatRate": obj["float_index"],
+ f"{key1}Daycount": obj["float_daycount"],
+ f"{key1}Frequency": obj["float_payment_freq"],
+ f"{key1}PaymentBDC": obj["float_bdc"],
+ f"{key1}EffectiveDate": obj["effective_date"],
+ f"{key1}MaturityDate": obj["maturity_date"],
+ f"{key1}Notional": obj["notional"],
+ f"{key1}ResetArrears": "Y" if obj["float_arrears"] else "N",
+ f"{key1}Currency": obj["currency"],
+ f"{key1}FixingFrequency": obj["float_fixing_freq"],
+ f"{key1}InterestCalcMethod": obj["pay_interest_calc_method"],
+ f"{key2}LegRateType": "Fixed",
+ f"{key2}FixedRate": obj["fixed_rate"],
+ f"{key2}Daycount": obj["fixed_daycount"],
+ f"{key2}Frequency": obj["fixed_payment_freq"],
+ f"{key2}PaymentBDC": obj["fixed_bdc"],
+ f"{key2}EffectiveDate": obj["effective_date"],
+ f"{key2}MaturityDate": obj["maturity_date"],
+ f"{key2}Notional": obj["notional"],
+ f"{key2}Currency": obj["currency"],
+ }
+ obj["Deal Type"] = "InterestRateSwapDeal"
+ obj["Action"] = "NEW" # Need to figure this out
+ obj["Client"] = "Serenitas"
+ obj["State"] = "Valid"
+ obj.update(d)
+ return obj
+
+ def to_citco(self, action):
+ obj = super().to_citco(action)
+ obj["ExecutionBroker"] = _citco_cp_cdea[obj["ExecutionBroker"]]
+ obj["SecurityType"] = "IRS"
+ obj["StrategyCode"] = f"{obj['portfolio']}/{obj['folder']}"
+ obj["FillPrice"] = obj["AvgPrice"]
+ obj["BuySellShortCover"] = "B" if obj["payreceive"] else "S"
+ obj["IDSource"] = "USERID"
+ obj["SecurityID"] = self.product.dealid
+ warnings.warn("Query DB")
+ obj["ClearingAgent"] = "BOA_FC"
+ return obj
+
+ @property
+ def product(self):
+ return IRSProduct(
+ birth_date=self.trade_date,
+ death_date=self.maturity_date,
+ fixed_rate=self.fixed_rate,
+ float_index=self.float_index,
+ )
+
+
+from enum import IntEnum
+
+
+class TrancheType(IntEnum):
+ DayCount = 3
+
+
+@dataclass
+class TrancheProduct(
+ Deal,
+ CitcoProduct,
+ deal_type=DealType.TrancheProduct,
+ table_name="citco_tranche",
+ product_key=("underlying_security_id", "attach", "detach"),
+ insert_ignore=(
+ "id",
+ "dealid",
+ "birth_date",
+ "death_date",
+ "security_desc",
+ "coupon",
+ "currency",
+ ),
+):
+ underlying_security_id: str = field(metadata={"citco": "UnderlyingSecurityId"})
+ attach: float = field(metadata={"citco": "Attachment_Points"})
+ detach: float = field(metadata={"citco": "Detachment_Points"})
+ birth_date: datetime.date = field(
+ default=None, metadata={"insert": False, "citco": "Birth_date"}
+ )
+ death_date: datetime.date = field(
+ default=None, metadata={"insert": False, "citco": "Death_date"}
+ )
+ coupon: float = field(
+ default=None, metadata={"insert": False, "citco": "CouponRate"}
+ )
+ security_desc: str = field(
+ default=None, metadata={"insert": False, "citco": "Sec_Desc"}
+ )
+ currency: str = field(default=None, metadata={"citco": "LocalCcy", "insert": False})
+ instrument_type: str = field(default="CDS", metadata={"citco": "InstrumentType"})
+ underlying_id_source: str = field(
+ default="RED", metadata={"citco": "UnderlyingIDSource"}
+ )
+ committed: bool = field(default=False)
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "UniqueIdentifier"}
+ )
+
+ def __post_init__(self):
+ if not all(
+ [
+ self.birth_date,
+ self.death_date,
+ self.coupon,
+ self.security_desc,
+ self.currency,
+ ]
+ ):
+ redcode, tenor = self.underlying_security_id.split("_")
+ tenor_yr = tenor + "yr"
+ sql_str = (
+ "SELECT issue_date, maturity, coupon, index, series "
+ "FROM index_desc WHERE tenor=%s AND redindexcode=%s"
+ )
+ with self._conn.cursor() as c:
+ c.execute(sql_str, (tenor_yr, redcode))
+ (
+ self.birth_date,
+ self.death_date,
+ self.coupon,
+ index,
+ series,
+ ) = c.fetchone()
+ self.security_desc = (
+ f"{desc_str(index, series, tenor)} {self.attach}-{self.detach}"
+ )
+ self.currency = "EUR" if index in ("XO", "EU") else "USD"
+ self.get_productid()
+
+ def to_citco(self, action):
+ if not self.id:
+ self.stage()
+ self.commit()
+ self.get_productid()
+ obj = super().to_citco(action)
+ obj["Command"] = "N"
+ obj["Active"] = "Y"
+ obj["CouponRate"] = obj["CouponRate"] / 100
+ obj["SettleDays"] = 3
+ obj["AccruStartDate"] = obj["Birth_date"]
+ return obj
+
+
+@dataclass
+class SwaptionProduct(
+ Deal,
+ CitcoProduct,
+ deal_type=DealType.SwaptionProduct,
+ table_name="citco_swaption",
+ product_key=(
+ "underlying_security_id",
+ "strike",
+ "expiration",
+ "callput",
+ "birth_date",
+ "death_date",
+ ),
+ insert_ignore=(
+ "id",
+ "dealid",
+ "security_desc",
+ "currency",
+ ),
+):
+ underlying_security_id: str = field(metadata={"citco": "UnderlyingSecurityId"})
+ security_desc: str = field(
+ init=False, metadata={"insert": False, "citco": "Sec_Desc"}
+ )
+ currency: str = field(
+ init=False, default=None, metadata={"citco": "LocalCcy", "insert": False}
+ )
+ instrument_type: str = field(metadata={"citco": "InstrumentType"})
+ callput: bool
+ strike: float = field(metadata={"citco": "StrikePrice"})
+ expiration: datetime.date = field(metadata={"citco": "ExpirationDate"})
+ underlying_id_source: str = field(
+ default="RED", metadata={"citco": "UnderlyingIDSource"}
+ )
+ birth_date: datetime.date = field(default=None, metadata={"citco": "Birth_date"})
+ death_date: datetime.date = field(default=None, metadata={"citco": "Death_date"})
+
+ committed: bool = field(default=False)
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "UniqueIdentifier"}
+ )
+
+ def __post_init__(self):
+ if self.instrument_type == "BNDO":
+ sql_str = "SELECT issue_date, maturity, coupon, index, series FROM index_desc WHERE tenor='5yr' AND redindexcode=%s"
+ with self._conn.cursor() as c:
+ c.execute(sql_str, (self.underlying_security_id.removesuffix("_5"),))
+ (
+ self.birth_date,
+ self.death_date,
+ self.coupon,
+ index,
+ series,
+ ) = c.fetchone()
+ self.security_desc = f"{desc_str(index, series, '5')} {self.expiration}-{self.strike}-{'C' if self.callput else 'P'}-{self.birth_date}-{self.death_date}"
+ self.currency = "EUR" if index in ("XO", "EU") else "USD"
+ elif self.instrument_type == "SWPO":
+ self.security_desc = ""
+ self.get_productid()
+
+ def to_citco(self):
+ if not self.id:
+ self.stage()
+ self.commit()
+ self.get_productid()
+ obj = super().to_citco(action)
+ if (
+ self.underlying_id_source == "USERID"
+ ): # Implies this is a Interest Rate Swaption
+ irs = IRSProduct(
+ birth_date=self.birth_date,
+ death_date=self.death_date,
+ fixed_rate=self.strike,
+ float_index=self.underlying_security_id,
+ )
+ irs.citco_stage()
+ obj["UnderlyingSecurityId"] = irs.dealid
+ obj["Command"] = "N"
+ obj["Active"] = "Y"
+ obj["ExpirationDate"] = obj["ExpirationDate"].strftime("%Y%m%d")
+ obj["Put/CallFlag"] = "C" if obj["callput"] else "P"
+ obj["OptionType"] = "Vanilla European"
+ return obj
+
+
+_citco_frequency = {"Yearly": 1, "Daily": 9, "Quarterly": 3}
+_citco_bdc = {"Modified Following": 4}
+_citco_daycount = {"ACT/360": 2}
+_citco_ratesource = {"SOFRRATE": 17819}
+_citco_cp_isda = {
+ "MSCSNY": "MS_IS",
+ "GOLDNY": "GS_IS",
+ "BAMSNY": "BOA_IS",
+ "BNPBNY": "BNP_IS",
+ "JPCBNY": "JPM_IS",
+}
+_citco_cp_cdea = {
+ "MSCSNY": "MS_CD",
+ "GOLDNY": "GS_CD",
+ "BAMSNY": "BOA_CD",
+ "BNPBNY": "BNP_CD",
+ "JPCBNY": "JPM_CD",
+ "CSFBBO": "CS_CD",
+ "CITINY": "CIT_CD",
+ "BARCNY": "BAR_CD",
+}
+
+
+@dataclass
+class IRSProduct(
+ Deal,
+ CitcoProduct,
+ deal_type=DealType.IRSProduct,
+ table_name="citco_irs",
+ product_key=("birth_date", "death_date", "float_index", "fixed_rate"),
+ insert_ignore=("id", "dealid", "security_desc"),
+):
+ birth_date: datetime.date = field(metadata={"citco": "Birth_date"})
+ death_date: datetime.date = field(metadata={"citco": "Death_date"})
+ fixed_rate: float
+ instrument_type: str = field(default="IRS", metadata={"citco": "InstrumentType"})
+ active: str = field(default=True, metadata={"citco": "Active"})
+ fixed_daycount: str = field(default="ACT/360")
+ fixed_payment_freq: str = field(default="Yearly")
+ fixed_bdc: str = field(default="Modified Following")
+ float_index: str = field(default="SOFRRATE")
+ float_daycount: str = field(default="ACT/360")
+ float_payment_freq: str = field(default="Yearly")
+ float_bdc: str = field(default="Modified Following")
+ currency: str = field(default="USD", metadata={"citco": "LocalCcy"})
+ float_fixing_freq: str = field(default="Daily")
+ pay_interest_calc_method: str = field(default="Compound")
+ committed: bool = field(default=False)
+ security_desc: str = field(
+ init=False, metadata={"insert": False, "citco": "Sec_Desc"}, default=None
+ )
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "UniqueIdentifier"}
+ )
+
+ def __post_init__(self):
+ self.get_productid()
+ self.security_desc = f"SWAP IRS {self.float_index}-{self.fixed_rate}-{self.birth_date}-{self.death_date}"
+
+ def to_citco(self):
+ if not self.id:
+ self.stage()
+ self.commit()
+ self.get_productid()
+
+ obj = super().to_citco(action)
+ d = {
+ "S_P_CurrencyCode": self.currency,
+ "S_P_PaymentFreqID": _citco_frequency[self.fixed_payment_freq],
+ "S_P_RateIndexID": 0,
+ "S_P_AccrualMethodID": _citco_daycount[self.fixed_daycount],
+ "S_P_InterestRate": self.fixed_rate,
+ "S_P_DayConventionID": _citco_bdc[self.fixed_bdc],
+ "S_P_ResetFreqID": 0,
+ "S_R_CurrencyCode": self.currency,
+ "S_R_PaymentFreqID": _citco_frequency[self.float_payment_freq],
+ "S_R_RateIndexID": 28,
+ "S_R_AccrualMethodID": _citco_daycount[self.float_daycount],
+ "S_R_InterestRate": 0,
+ "S_R_DayConventionID": _citco_bdc[self.float_bdc],
+ "S_R_ResetFreqID": _citco_frequency[self.float_fixing_freq],
+ "S_R_RateSource": _citco_ratesource[self.float_index],
+ }
+ obj.update(d)
+ obj["Command"] = "N"
+ obj["Active"] = "Y" if obj["Active"] else "N"
+ obj["PrincipalExchTypeID"] = 1
+ return obj
+
+
+@dataclass
+class TRSProduct(
+ Deal,
+ CitcoProduct,
+ deal_type=DealType.TRSProduct,
+ table_name="citco_trs",
+ product_key=("birth_date", "death_date", "funding_index", "underlying_security"),
+ insert_ignore=("id", "dealid", "security_desc"),
+):
+ birth_date: datetime.date = field(metadata={"citco": "Birth_date"})
+ death_date: datetime.date = field(metadata={"citco": "Death_date"})
+ underlying_security: str = field(metadata={"citco": "UnderlyingSecurityId"})
+ active: str = field(default=True, metadata={"citco": "Active"})
+ funding_daycount: str = field(default="ACT/360")
+ funding_freq: str = field(default="Quarterly")
+ funding_payment_roll_convention: str = field(default="Modified Following")
+ asset_daycount: str = field(default="ACT/360")
+ asset_freq: str = field(default="Quarterly")
+ asset_payment_roll_convention: str = field(default="Modified Following")
+ currency: str = field(default="USD", metadata={"citco": "LocalCcy"})
+ interest_calc_method: str = field(default="Compound")
+ compound_avg_frequency: str = field(default="Daily")
+ fixing_frequency: str = field(default="Daily")
+ committed: bool = field(default=False)
+ instrument_type: str = field(default="TRS", metadata={"citco": "InstrumentType"})
+ funding_index: str = field(default="SOFRRATE", metadata={})
+ security_desc: str = field(
+ init=False, metadata={"insert": False, "citco": "Sec_Desc"}, default=None
+ )
+ id: int = field(default=None, metadata={"insert": False})
+ dealid: str = field(
+ default=None, metadata={"insert": False, "citco": "UniqueIdentifier"}
+ )
+
+ def __post_init__(self):
+ self.get_productid()
+ _citco_trs = {"4J623JAA8": "IBOXHY_TRS"}
+ self.security_desc = f"{_citco_trs[self.underlying_security]}-{self.funding_index}-{self.birth_date}-{self.death_date}"
+
+ def to_citco(self):
+ if not self.id:
+
+ self.stage()
+ self.commit()
+ self.get_productid()
+ obj = super().to_citco(action)
+ d = {
+ f"S_P_CurrencyCode": self.currency,
+ f"S_P_PaymentFreqID": _citco_frequency[self.funding_freq],
+ f"S_P_RateIndexID": 28,
+ f"S_P_AccrualMethodID": _citco_daycount[self.funding_daycount],
+ f"S_P_InterestRate": 0,
+ f"S_P_PaymentCalandarID": 3,
+ f"S_P_DayConventionID": _citco_bdc[self.funding_payment_roll_convention],
+ f"S_P_ResetFreqID": _citco_frequency[self.funding_freq],
+ f"S_P_RateSourceID": _citco_ratesource[self.funding_index],
+ f"S_R_CurrencyCode": self.currency,
+ f"S_R_PaymentFreqID": _citco_frequency[self.asset_freq],
+ f"S_R_RateIndexID": 0,
+ f"S_R_AccrualMethodID": _citco_daycount[self.asset_daycount],
+ f"S_R_InterestRate": 0,
+ f"S_R_PaymentCalandarID": 3,
+ f"S_R_DayConventionID": _citco_bdc[self.asset_payment_roll_convention],
+ f"S_R_ResetFreqID": _citco_frequency[self.asset_freq],
+ f"S_R_RateSourceID": 0,
+ }
+ obj.update(d)
+ obj["Command"] = "N"
+ obj["Active"] = "Y" if obj["Active"] else "N"
+ obj["GeneralDirection"] = "F"
+ obj["PrincipalExchTypeID"] = 3
+ obj["UnderlyingIDSource"] = "RED"
+ return obj