import datetime import os import pandas as pd import psycopg2 import redis import socket from flask import (request, render_template, redirect, url_for, send_from_directory, send_file, g, jsonify) from .models import (ModelForm, CASH_STRAT, CCY, BondDeal, CDSDeal, SwaptionDeal, FutureDeal, CashFlowDeal, Counterparties, Accounts) from sqlalchemy.exc import IntegrityError from wtforms.fields import BooleanField from pandas.tseries.offsets import CustomBusinessDay from pandas.tseries.holiday import get_calendar, HolidayCalendarFactory, GoodFriday from .utils import bump_rev, simple_serialize from PyPDF2 import PdfFileMerger from io import BytesIO from . import app from . import db fed_cal = get_calendar('USFederalHolidayCalendar') bond_cal = HolidayCalendarFactory('BondCalendar', fed_cal, GoodFriday) bus_day = CustomBusinessDay(calendar=bond_cal()) def cp_choices(kind='bond'): if kind == 'bond': return (Counterparties.query.order_by('name'). with_entities(Counterparties.code, Counterparties.name)) elif kind == 'future': return [] elif kind == 'cds': return (Counterparties.query. order_by('name'). filter(Counterparties.name.ilike('%CDS%')). with_entities(Counterparties.code, Counterparties.name)) def account_codes(): return Accounts.query.order_by('code').with_entities(Accounts.code, Accounts.name) def get_queue(): q = getattr(g, 'queue', None) if q is None: hostname = socket.gethostname() if hostname == 'ziggy': q = g.queue = redis.Redis(unix_socket_path='/run/redis/redis.sock') else: q = g.queue = redis.Redis(host='ziggy') return q def get_db(): db = getattr(g, '_database', None) if db is None: db = g._database = psycopg2.connect(database="serenitasdb", user="serenitas_user", host="debian") return db @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) if db is not None: db.close() class CounterpartyForm(ModelForm): class Meta: model = Counterparties include_primary_keys = True class BondForm(ModelForm): upload_globeop = BooleanField(label="Upload to globeop?") class Meta: model = BondDeal include_foreign_keys = True exclude = ['dealid', 'lastupdate', #we generate it with a trigger at the server level 'principal_payment', 'accrued_payment'] class CDSForm(ModelForm): upload_globeop = BooleanField(label="Upload to globeop?") class Meta: model = CDSDeal include_foreign_keys = True exclude = ['dealid', 'lastupdate'] class SwaptionForm(ModelForm): upload_globeop = BooleanField(label="Upload to globeop?") class Meta: model = SwaptionDeal include_foreign_keys = True exclude = ['dealid', 'lastupdate'] class FutureForm(ModelForm): upload_globeop = BooleanField(label="Upload to globeop?") class Meta: model = FutureDeal include_foreign_keys = True exclude = ['dealid', 'lastupdate'] def get_deal(kind): if kind == 'cds': return CDSDeal elif kind == 'bond': return BondDeal elif kind == 'swaption': return SwaptionDeal elif kind == 'future': return FutureDeal else: raise RuntimeError('Unknown Deal type') def _get_form(kind): if kind == 'cds': return CDSForm elif kind == 'bond': return BondForm elif kind == 'swaption': return SwaptionForm elif kind == 'future': return FutureForm else: raise RuntimeError('Unknown Deal type') def get_form(trade, kind): Form = _get_form(kind) if trade.id: form = Form(obj=trade) else: today = pd.datetime.today() if kind == 'cds': tomorrow = today + pd.DateOffset(1) form = Form(trade_date=today.date(), effective_date=tomorrow.date(), upfront_settle_date=today.date() + 3 * bus_day) else: form = Form(trade_date=today.date(), settle_date=today.date() + 2 * bus_day) #add extra empty fields empty_choice = (None, '') for attr in ['folder', 'buysell', 'asset_class', 'swaption_type']: try: dropdown = getattr(form, attr) if dropdown.choices[0] != empty_choice: dropdown.choices.insert(0, empty_choice) except AttributeError: continue return form def get_wire_form(wire): if wire.id: return WireForm(obj=trade) else: today = pd.datetime.today() form = WireForm(trade_date=today.date()) return form def get_trade(tradeid, kind): Deal = get_deal(kind) return Deal.query.get(tradeid) if tradeid else Deal() def save_ticket(trade, old_ticket_name): if trade.ticket: if old_ticket_name: new_name = bump_rev(old_ticket_name) else: new_name = "{0} {1}.pdf".format(str(trade.trade_date), trade.description) trade.ticket.save(os.path.join(app.config['TICKETS_FOLDER'], new_name)) trade.ticket = new_name else: trade.ticket = old_ticket_name def split_direction(g, direction): if direction == "outgoing": return [{"folder": cf.folder, "amount": -cf.amount, "code": cf.code} for cf in g if cf.amount < 0] elif direction == "incoming": return [{"folder": cf.folder, "amount": cf.amount, "code": cf.code} for cf in g if cf.amount > 0] else: raise ValueError("direction can be one of 'outgoing' or 'incoming'") def gen_cashflow_deals(form, session): action = form.get("action") to_date = lambda s: datetime.datetime.strptime(s, "%Y-%m-%d") trade_date = form.get("trade_date", None, to_date) for direction in ["incoming", "outgoing"]: count = 1 while True: if f"{direction}-code-{count}" not in form: break else: d = {field: form.get(f"{direction}-{field}-{count}") for field \ in ["folder", "code", "amount", "currency"]} count += 1 if direction == "outgoing": d['amount'] = - float(d['amount']) elif direction == "incoming": d['amount'] = float(d['amount']) else: raise ValueError("direction needs to be 'outgoing' or 'incoming'") cf = CashFlowDeal(trade_date=trade_date, action=action, **d) session.add(cf); yield cf @app.route('/wires/', methods = ['GET', 'POST']) @app.route('/wires/', defaults={'wire_id': None}, methods=['GET', 'POST']) def wire_manage(wire_id): if request.method == 'POST': wires = list(gen_cashflow_deals(request.form, db.session)) try: db.session.commit() except IntegrityError as e: app.logger.error(e) db.session.rollback() return render_template('wire_entry.html', strategies=CASH_STRAT.enums, currencies=CCY.enums, accounts=account_codes(), outgoing_wires=split_direction(wires, "outgoing"), incoming_wires=split_direction(wires, "incoming"), action=request.form.get('action'), trade_date=request.form.get('trade_date')) else: if request.form.get('upload_globeop') == 'y': q = get_queue() for wire in wires: q.rpush('wires', simple_serialize(wire)) return redirect(url_for('list_trades', kind='wire')) wire = CashFlowDeal() if wire_id is None else CashFlowDeal.query.get(wire_id) return render_template('wire_entry.html', strategies=CASH_STRAT.enums, currencies=CCY.enums, accounts=account_codes(), outgoing_wires=split_direction([wire], "outgoing") if wire_id else [], incoming_wires=split_direction([wire], "incoming") if wire_id else [], trade_date=wire.trade_date if wire_id else datetime.date.today(), action_url=url_for('wire_manage', wire_id=wire_id), action=wire.action if wire_id else None) @app.route('/trades//', methods=['GET', 'POST']) @app.route('/trades//', defaults = {'tradeid': None}, methods=['GET', 'POST']) @app.route('/trades/', defaults = {'tradeid': None, 'kind': 'bond'}, methods=['GET', 'POST']) def trade_manage(tradeid, kind): trade = get_trade(tradeid, kind) form = _get_form(kind)() form.cp_code.choices = form.cp_code.choices + list(cp_choices(kind)) if kind == 'bond': old_ticket_name = trade.ticket if form.validate_on_submit(): form.populate_obj(trade) session = form.get_session() if not tradeid: session.add(trade) if kind == 'bond': save_ticket(trade, old_ticket_name) try: session.commit() except IntegrityError as e: app.logger.error(e) session.rollback() return render_template('trade_entry.html', form=form, action_url= url_for('trade_manage', tradeid=tradeid, kind=kind)) else: if form.upload_globeop.data: q = get_queue() q.rpush(f'{kind}_trades', simple_serialize(trade)) return redirect(url_for('list_trades', kind=kind)) else: form = get_form(trade, kind) form.cp_code.choices = form.cp_code.choices + list(cp_choices(kind)) return render_template('trade_entry.html', form=form, action_url=url_for('trade_manage', tradeid=tradeid, kind=kind)) @app.route('/', defaults={'kind': 'bond'}) @app.route('/blotter/') @app.route('/blotter/', defaults={'kind': 'bond'}) def list_trades(kind): if kind == 'wire': Deal = CashFlowDeal else: Deal = get_deal(kind) trade_list = Deal.query.order_by(Deal.trade_date.desc(), Deal.id.desc()) return render_template(f'{kind}_blotter.html', trades=trade_list.all()) @app.route('/tickets/') def download_ticket(tradeid): trade = BondDeal.query.get(tradeid) pdf = PdfFileMerger() pdf.append(os.path.join(app.config['TICKETS_FOLDER'], trade.ticket)) pdf.append(os.path.join(app.config['CP_FOLDER'], trade.counterparty.instructions)) fh = BytesIO() pdf.write(fh) pdf.close() fh.seek(0) return send_file(fh, mimetype='application/pdf') @app.route('/counterparties/', methods = ['GET']) @app.route('/counterparties/', defaults={'instr': None}, methods=['GET']) def list_counterparties(instr): if instr: return send_from_directory(filename=instr, directory=app.config['CP_FOLDER'], mimetype='application/pdf') else: cp_list = Counterparties.query.order_by(Counterparties.name) return render_template('counterparties.html', counterparties=cp_list.all()) @app.route('/edit_cp/', methods=['GET', 'POST']) @app.route('/edit_cp/', defaults={'cpcode': None}, methods=['GET', 'POST']) def edit_counterparty(cpcode): if cpcode: cp = Counterparties.query.get(cpcode) else: cp = Counterparties() cp_form = CounterpartyForm() old_instructions = cp.instructions or None if cp_form.validate_on_submit(): cp_form.populate_obj(cp) session = cp_form.get_session() if not cpcode: session.add(cp) instructions = cp_form.instructions if not instructions.data: cp.instructions = old_instructions else: cp.instructions = cp.name + '.pdf' instructions.save(os.path.join(app.config['CP_FOLDER'], cp.instructions)) session.commit() return redirect(url_for('list_counterparties')) else: return render_template('edit_cp.html', form=CounterpartyForm(obj=cp), code=cpcode) @app.route('/_ajax', methods=['GET']) def get_bbg_id(): bbg_id = request.args.get('bbg_id') try: _, indextype, _, series, tenor = bbg_id.split() except ValueError: return "not a valid bloomberg description", 400 indextype = indextype[:2] tenor = tenor[:-1] + 'yr' series = int(series[1:]) sqlstr1 = "SELECT * FROM index_redcode(%s::index_type, %s::smallint, %s)" sqlstr2 = "SELECT maturity, coupon FROM index_maturity WHERE index=%s and series=%s and tenor=%s" db = get_db() with db.cursor() as c: c.execute(sqlstr1, (indextype, series, pd.datetime.today().date())) (redcode,) = c.fetchone() c.execute(sqlstr2, (indextype, series, tenor)) maturity, coupon = c.fetchone() return jsonify({'maturity': maturity.strftime('%Y-%m-%d'), 'redcode': redcode, 'coupon': coupon})