diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/Dawn/__init__.py | 5 | ||||
| -rw-r--r-- | python/Dawn/models.py | 6 | ||||
| -rw-r--r-- | python/Dawn/static/wire.js | 42 | ||||
| -rw-r--r-- | python/Dawn/templates/wire_blotter.html | 25 | ||||
| -rw-r--r-- | python/Dawn/templates/wire_entry.html | 149 | ||||
| -rw-r--r-- | python/Dawn/views.py | 155 | ||||
| -rw-r--r-- | python/dawn_utils.py | 3 |
7 files changed, 279 insertions, 106 deletions
diff --git a/python/Dawn/__init__.py b/python/Dawn/__init__.py index 87be366c..d571f5aa 100644 --- a/python/Dawn/__init__.py +++ b/python/Dawn/__init__.py @@ -1,10 +1,13 @@ -from flask import Flask import logging + +from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy import MetaData +from flask_wtf.csrf import CSRFProtect app = Flask(__name__) app.config.from_envvar('CONF') +csrf = CSRFProtect(app) db = SQLAlchemy(app, metadata=MetaData(schema=app.config['SCHEMA'])) if not app.debug: diff --git a/python/Dawn/models.py b/python/Dawn/models.py index fd33926b..4a5727b9 100644 --- a/python/Dawn/models.py +++ b/python/Dawn/models.py @@ -35,7 +35,7 @@ class Accounts(db.Model): name = db.Column(db.String) custodian = db.Column(db.String) cash_account = db.Column(db.String) - counterpaty = db.Column(db.String, db.ForeignKey('counterparties.code')) + counterparty = db.Column(db.String, db.ForeignKey('counterparties.code')) BOND_STRAT = ENUM('M_STR_MAV', 'M_STR_MEZZ', 'CSO_TRANCH', 'M_CLO_BB20', 'M_CLO_AAA', 'M_CLO_BBB', 'M_MTG_IO', 'M_MTG_THRU', @@ -260,9 +260,7 @@ class CashFlowDeal(db.Model): lastupdate = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) action = db.Column(ACTION) folder = db.Column(CASH_STRAT, nullable=False) - code = db.Column(db.String(5), db.ForeignKey('accounts.code'), - info={'choices': [('IB', 'pomme')], - 'label': 'account'}, nullable=False) + code = db.Column(db.String(5), db.ForeignKey('accounts.code'), nullable=False) amount = db.Column(db.Float, nullable=False) trade_date = db.Column(db.Date, nullable=False) settle_date = db.Column(db.Date, nullable=False) diff --git a/python/Dawn/static/wire.js b/python/Dawn/static/wire.js new file mode 100644 index 00000000..37f2a1c8 --- /dev/null +++ b/python/Dawn/static/wire.js @@ -0,0 +1,42 @@ +var outgoing_count = 0; +var incoming_count = 0; + +function addWire(direction, wire_data) { + return function() { + if( this != window.window) { + this.remove(); + } + var template = document.getElementById("fragment"); + var clone = document.importNode(template.content, true); + count = direction == "outgoing" ? ++outgoing_count : ++incoming_count; + + clone.querySelectorAll("label").forEach( + label => label.setAttribute("for", + direction + "-" + label.getAttribute("for") + "-" + count)); + clone.querySelectorAll(".form-control").forEach( + elem => {elem.id = direction + "-" + elem.id + "-" + count; + elem.setAttribute("name", + direction + "-" + elem.getAttribute("name") + "-" + count) + }); + clone.getElementById("btn").id = direction + "-btn"; + document.getElementById(direction).appendChild(clone); + for(var key in wire_data) { + document.getElementById(direction + "-" + key + "-" + count).value = wire_data[key]; + } + document.getElementById(direction + "-btn").addEventListener("click", addWire(direction)); + } +} + +$(function() { + for (let w of outgoing_wires) { + addWire("outgoing", w)(); + } + for (let w of incoming_wires) { + console.log(w); + addWire("incoming", w)(); + } + if (outgoing_wires.length == 0 && incoming_wires.length == 0) { + addWire("outgoing")(); + addWire("incoming")(); + } +}) diff --git a/python/Dawn/templates/wire_blotter.html b/python/Dawn/templates/wire_blotter.html index 8397e5df..9f70621d 100644 --- a/python/Dawn/templates/wire_blotter.html +++ b/python/Dawn/templates/wire_blotter.html @@ -6,34 +6,19 @@ <td>Deal ID</td> <td>Trade Date</td> <td>Settle Date</td> - <td>Buy/Sell</td> - <td>Quantity</td> - <td>Type</td> - <td>Maturity</td> - <td>Price</td> - <td>Commission</td> - <td>Description</td> - <td>Ticker</td> - <td>Counterparty</td> <td>Strategy</td> + <td>Account</td> + <td>Amount</td> </tr> </thead> {% for trade in trades %} <tr> - <td><a href="{{url_for('wire_manage', wire_id=wire.id)}}">{{wire.dealid}}</a></td> + <td><a href="{{url_for('wire_manage', wire_id=trade.id)}}">{{trade.dealid}}</a></td> <td>{{trade.trade_date}}</td> <td>{{trade.settle_date}}</td> - <td>{% if trade.buysell %}Buy{% else %}Sell{% endif %}</td> - <td>{{trade.quantity}}</td> - <td>{{trade.swap_type}}</td> - <td>{{trade.maturity}}</td> - <td>{{trade.price}}</td> - <td>{{trade.commission}}</td> - <td>{{trade.security_desc}}</td> - <td>{{trade.bbg_ticker}}</td> - <td><a href="{{url_for('edit_counterparty', - cpcode=trade.counterparty.code)}}">{{trade.counterparty.name}}</a></td> <td>{{trade.folder}}</td> + <td>{{trade.account.name}}</td> + <td>{{trade.amount}}</td> </tr> {% endfor %} </table> diff --git a/python/Dawn/templates/wire_entry.html b/python/Dawn/templates/wire_entry.html index a5da6b2b..84176890 100644 --- a/python/Dawn/templates/wire_entry.html +++ b/python/Dawn/templates/wire_entry.html @@ -4,48 +4,149 @@ <meta charset="utf-8"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> </head> - <body style="max-width:1024px; margin:0 auto"> - <datalist id="index_list"></datalist> + <body style="max-width:512px; margin:20px auto"> <form method="POST" class="form-horizontal" action="{{action_url}}" enctype="multipart/form-data"> - {% for field in form if field.type != 'BooleanField' %} - <div class="form-group {% if field.id in form.errors %}has-error{% endif %}"> - {% if field.type != 'CSRFTokenField' %} - <label class="control-label col-md-2" for="{{ field.id }}"> - {{ field.label.text }} + <div class="form-group "> + <label class="control-label col-md-4" for="action"> + action </label> - {% endif %} - <div class="col-md-3"> - {{ field(class_="form-control") }} + <div class="col-md-6"> + <select class="form-control" id="action" name="action"> + <option value="NEW" + {% if action and action == 'NEW' %} + selected="selected" + {% endif %}>NEW</option> + <option value="UPDATE" + {% if action and action == 'UPDATE' %} + selected="selected" + {% endif %}>UPDATE</option> + <option value="CANCEL" + {% if action and action == 'CANCEL' %} + selected="selected" + {% endif %}>CANCEL</option> + </select> + </div> + </div> + <fieldset id="outgoing"> + <legend>Outgoing amounts</legend> + </fieldset> + <fieldset id="incoming"> + <legend>Incoming amounts</legend> + </fieldset> + <hr> + <div class="form-group"> + </div> + + <div class="form-group"> + <label class="control-label col-md-4" for="trade_date"> + trade_date + </label> + + <div class="col-md-6"> + <input class="form-control" id="trade_date" name="trade_date" value="{{trade_date}}" type="date"> </div> - {% if field.id in form.errors %} + </div> + + <div class="form-group"> + + <label class="control-label col-md-4" for="settle_date"> + settle_date + </label> + + <div class="col-md-6"> + <input class="form-control" id="settle_date" name="settle_date" value="{{settle_date}}" type="date"> + </div> + + </div> + + <div class="form-group "> + <div class="col-md-3"> - {{form.errors[field.id][0]}} - </div>{% endif %} + <input class="form-control" id="csrf_token" name="csrf_token" value="{{csrf_token()}}" type="hidden"> + </div> + </div> - {% endfor %} + <div class="form-group"> - <div class="col-md-offset-2 col-md-3"> + <div class="col-md-offset-4 col-md-6"> <div class="checkbox"> <label> - <input id="upload_globeop" name="upload_globeop" type="checkbox" value="y">Upload to globeop? + <input id="upload_globeop" name="upload_globeop" value="y" type="checkbox">Upload to globeop? </label> </div> </div> </div> <div class="form-group"> - <div class="col-md-offset-2 col-md-3"> + <div class="col-md-offset-4 col-md-6"> <button type="submit" class="btn btn-default">Submit</button> </div> </div> </form> - {% if 'cds' or 'swaption' in action_url %} - <script type="text/javascript" src="https://code.jquery.com/jquery-2.2.0.min.js"></script> - <script type="text/javascript" - src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" - integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" + <template id="fragment"> + <div class="form-group"> + <label class="control-label col-md-4" for="folder"> + folder + </label> + <div class="col-md-6"> + <select class="form-control" id="folder" name="folder"> + <option value="None">{{strat}}</option> + {% for strat in strategies %} + <option value="{{strat[0]}}">{{strat[1]}}</option> + {% endfor %} + </select> + </div> + + </div> + <div class="form-group "> + + <label class="control-label col-md-4" for="code"> + account + </label> + + <div class="col-md-6"> + <select class="form-control" id="code" name="code"> + <option value="None"></option> + {% for account in accounts %} + <option value="{{account[0]}}">{{account[1]}}</option> + {% endfor %} + </select> + </div> + + </div> + + <div class="form-group "> + <label class="control-label col-md-4" for="amount">amount</label> + + <div class="col-md-6"> + <input class="form-control" id="amount" name="amount" value="" type="text"> + </div> + <div class="col-md-2"> + <input id="btn" type="button" class="btn" value="+"> + </div> + </div> + </template> + <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" + integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" + crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" + integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" + crossorigin="anonymous"></script> + <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" + integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script> - <script type="text/javascript" src="{{ url_for('static', filename='dawn.js') }}"></script> - {% endif %} + <script type="text/javascript" src="{{ url_for('static', filename='wire.js') }}"></script> + <script type="text/javascript"> + {% if outgoing_wires is defined %} + var outgoing_wires = {{outgoing_wires|tojson|safe}} + {% else %} + var outgoing_wires = []; + {% endif %} + {% if incoming_wires is defined %} + var incoming_wires = {{incoming_wires|tojson|safe}} + {% else %} + var incoming_wires = []; + {% endif %} + </script> </body> </html> diff --git a/python/Dawn/views.py b/python/Dawn/views.py index 761fdd5b..5acec034 100644 --- a/python/Dawn/views.py +++ b/python/Dawn/views.py @@ -1,23 +1,28 @@ +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, + +from .models import (ModelForm, CASH_STRAT, BondDeal, CDSDeal, SwaptionDeal, FutureDeal, CashFlowDeal, Counterparties, Accounts) from sqlalchemy.exc import IntegrityError from wtforms.fields import BooleanField -import pandas as pd + from pandas.tseries.offsets import CustomBusinessDay from pandas.tseries.holiday import get_calendar, HolidayCalendarFactory, GoodFriday -import os -import datetime -import redis + from .utils import load_counterparties, bump_rev, simple_serialize from PyPDF2 import PdfFileMerger from io import BytesIO from . import app -import socket -import psycopg2 +from . import db fed_cal = get_calendar('USFederalHolidayCalendar') bond_cal = HolidayCalendarFactory('BondCalendar', fed_cal, GoodFriday) @@ -36,7 +41,7 @@ def cp_choices(kind='bond'): with_entities(Counterparties.code, Counterparties.name)) def account_codes(): - return Accounts.query.order_by('code').with_entities(Accounts.code, Accounts.code) + return Accounts.query.order_by('code').with_entities(Accounts.code, Accounts.name) def get_queue(): q = getattr(g, 'queue', None) @@ -96,13 +101,6 @@ class FutureForm(ModelForm): include_foreign_keys = True exclude = ['dealid', 'lastupdate'] -class WireForm(ModelForm): - upload_globeop = BooleanField(label="Upload to globeop?") - class Meta: - model = CashFlowDeal - include_foreign_keys = True - exclude = ['dealid', 'lastupdate'] - def get_deal(kind): if kind == 'cds': return CDSDeal @@ -164,9 +162,6 @@ def get_trade(tradeid, kind): Deal = get_deal(kind) return Deal.query.get(tradeid) if tradeid else Deal() -def get_wire(wiredid): - CashFlowDeal.query.get(wireid) - def save_ticket(trade, old_ticket_name): if trade.ticket: if old_ticket_name: @@ -180,42 +175,88 @@ def save_ticket(trade, old_ticket_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): + action = form.get("action") + to_date = lambda s: datetime.datetime.strptime(s, "%Y-%m-%d") + trade_date = form.get("trade_date", None, to_date) + settle_date = form.get("settle_date", None, to_date) + for direction in ["incoming", "outgoing"]: + count = 1 + while True: + if f"{direction}-code-{count}" not in form: + break + else: + r = [form.get(f"{direction}-{field}-{count}") for field \ + in ["folder", "code", "amount"]] + count += 1 + if direction == "outgoing": + r[2] = -float(r[2]) + elif direction == "incoming": + r[2] = float(r[2]) + else: + raise ValueError("direction needs to be 'outgoing' or 'incoming'") + yield CashFlowDeal(trade_date=trade_date, + settle_date=settle_date, + action=action, + folder=r[0], + code=r[1], + amount=r[2]) + @app.route('/wires/<int:wire_id>', methods = ['GET', 'POST']) -@app.route('/wires/', defaults = {'wire_id': None}, methods = ['GET', 'POST']) +@app.route('/wires/', defaults={'wire_id': None}, methods=['GET', 'POST']) def wire_manage(wire_id): - if wire_id is None: - wire = CashFlowDeal() - else: - wire = CashFlowDeal().query.get(wire_id) - form = WireForm() - form.code.choices = form.code.choices + list(account_codes()) - if form.validate_on_submit(): - form.populate_obj(wire) - session = form.get_session() - if not wire_id: - session.add(wire) + if request.method == 'POST': + for wire in gen_cashflow_deals(request.form): + print(wire) + db.session.add(wire) + try: - session.commit() + db.session.commit() except IntegrityError as e: app.logger.error(e) - session.rollback() - return render_template('wire_entry.html', form=form, - action_url= - url_for('wire_manage', wire_id=wire_id)) + db.session.rollback() + return render_template('wire_entry.html', + strategies=[(e, e) for e in CASH_STRAT.enums], + accounts=account_codes(), + outgoing_wires=split_direction( + gen_cashflow_deals(request.form), "outgoing"), + incoming_wires=split_direction( + gen_cashflow_deals(request.form), "incoming"), + action=request.form.get('action'), + trade_date=request.form.get('trade_date'), + settle_date=request.form.get('settle_date')) else: - if form.upload_globeop.data: + if request.form.get('upload_globeop') == 'y': q = get_queue() - q.rpush('wires', simple_serialize(wire)) - return redirect(url_for('list_trades')) - else: - form = get_wire_form(wire) - form.code.choices = form.code.choices + list(account_codes()) - return render_template('wire_entry.html', form=form, - action_url = url_for('wire_manage', wire_id=wire_id)) + for wire in gen_cashflow_deals(request.form): + 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=[(e, e) for e in CASH_STRAT.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(), + settle_date=wire.settle_date if wire_id else '', + action_url=url_for('wire_manage', wire_id=wire_id), + action=wire.action if wire_id else None) + -@app.route('/trades/<kind>/<int:tradeid>', methods = ['GET', 'POST']) -@app.route('/trades/<kind>/', defaults = {'tradeid': None}, methods = ['GET', 'POST']) -@app.route('/trades/', defaults = {'tradeid': None, 'kind': 'bond'}, methods = ['GET', 'POST']) +@app.route('/trades/<kind>/<int:tradeid>', methods=['GET', 'POST']) +@app.route('/trades/<kind>/', 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)() @@ -241,24 +282,24 @@ def trade_manage(tradeid, kind): else: if form.upload_globeop.data: q = get_queue() - q.rpush('{0}_trades'.format(kind), simple_serialize(trade)) + 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)) + action_url=url_for('trade_manage', tradeid=tradeid, kind=kind)) @app.route('/', defaults={'kind': 'bond'}) @app.route('/blotter/<kind>') @app.route('/blotter/', defaults={'kind': 'bond'}) def list_trades(kind): if kind == 'wire': - Deal = CashFlowDeal() + Deal = CashFlowDeal else: Deal = get_deal(kind) trade_list = Deal.query.order_by(Deal.trade_date.desc(), Deal.id.desc()) - return render_template('{}_blotter.html'.format(kind), trades=trade_list.all()) + return render_template(f'{kind}_blotter.html', trades=trade_list.all()) @app.route('/tickets/<int:tradeid>') def download_ticket(tradeid): @@ -273,18 +314,18 @@ def download_ticket(tradeid): return send_file(fh, mimetype='application/pdf') @app.route('/counterparties/<path:instr>', methods = ['GET']) -@app.route('/counterparties/', defaults = {'instr': None}, 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'], + 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()) + return render_template('counterparties.html', counterparties=cp_list.all()) -@app.route('/edit_cp/<cpcode>', methods = ['GET', 'POST']) -@app.route('/edit_cp/', defaults = {'cpcode': None}, methods = ['GET', 'POST']) +@app.route('/edit_cp/<cpcode>', methods=['GET', 'POST']) +@app.route('/edit_cp/', defaults={'cpcode': None}, methods=['GET', 'POST']) def edit_counterparty(cpcode): if cpcode: cp = Counterparties.query.get(cpcode) @@ -310,7 +351,7 @@ def edit_counterparty(cpcode): else: return render_template('edit_cp.html', form=CounterpartyForm(obj=cp), code=cpcode) -@app.route('/_ajax', methods = ['GET']) +@app.route('/_ajax', methods=['GET']) def get_bbg_id(): bbg_id = request.args.get('bbg_id') try: diff --git a/python/dawn_utils.py b/python/dawn_utils.py index 1c49d591..60a90d1f 100644 --- a/python/dawn_utils.py +++ b/python/dawn_utils.py @@ -28,6 +28,9 @@ identifier = COALESCE(identifier, cusip, isin)', NEW.id); ELSIF (TG_TABLE_NAME = 'futures') THEN stub := 'SCFUT'; sqlstr := format(sqlstr, 'dealid = $1||id', NEW.id); + ELSIF (TG_TABLE_NAME = 'wires') THEN + stub := 'SCCSH'; + sqlstr := format(sqlstr, 'dealid = $1||id', NEW.id); END IF; EXECUTE sqlstr USING stub; RETURN NEW; |
