diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..331156b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3 + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY templates templates +COPY main.py main.py + +CMD [ "python", "main.py" ] diff --git a/cache.csv b/cache.csv new file mode 100644 index 0000000..5c2b6de --- /dev/null +++ b/cache.csv @@ -0,0 +1,102 @@ +Date,1 Mo,2 Mo,3 Mo,4 Mo,6 Mo,1 Yr,2 Yr,3 Yr,5 Yr,7 Yr,10 Yr,20 Yr,30 Yr +05/24/2024,5.56,5.53,5.46,5.51,5.44,5.21,4.93,4.71,4.53,4.49,4.46,4.65,4.57 +05/23/2024,5.51,5.48,5.46,5.51,5.44,5.2,4.91,4.71,4.52,4.5,4.47,4.67,4.58 +05/22/2024,5.49,5.48,5.45,5.5,5.43,5.16,4.86,4.64,4.47,4.44,4.43,4.63,4.55 +05/21/2024,5.5,5.47,5.45,5.51,5.42,5.14,4.82,4.61,4.43,4.42,4.41,4.65,4.55 +05/20/2024,5.5,5.47,5.45,5.51,5.43,5.15,4.82,4.62,4.46,4.44,4.44,4.68,4.58 +05/17/2024,5.5,5.47,5.46,5.5,5.41,5.14,4.83,4.6,4.44,4.43,4.42,4.66,4.56 +05/16/2024,5.51,5.47,5.45,5.53,5.41,5.13,4.78,4.58,4.4,4.39,4.38,4.62,4.52 +05/15/2024,5.49,5.46,5.45,5.5,5.4,5.1,4.73,4.51,4.35,4.35,4.36,4.61,4.52 +05/14/2024,5.5,5.47,5.44,5.51,5.43,5.16,4.81,4.62,4.46,4.45,4.45,4.69,4.59 +05/13/2024,5.5,5.47,5.45,5.51,5.43,5.16,4.85,4.66,4.5,4.49,4.48,4.72,4.63 +05/10/2024,5.51,5.47,5.47,5.51,5.43,5.17,4.87,4.65,4.52,4.51,4.5,4.74,4.64 +05/09/2024,5.51,5.48,5.46,5.5,5.41,5.12,4.8,4.6,4.47,4.46,4.45,4.7,4.6 +05/08/2024,5.51,5.47,5.45,5.5,5.41,5.13,4.84,4.63,4.5,4.49,4.48,4.73,4.64 +05/07/2024,5.51,5.48,5.45,5.51,5.41,5.13,4.82,4.6,4.48,4.47,4.47,4.7,4.61 +05/06/2024,5.51,5.48,5.45,5.5,5.42,5.12,4.82,4.64,4.48,4.48,4.49,4.73,4.64 +05/03/2024,5.51,5.48,5.45,5.5,5.41,5.12,4.81,4.63,4.48,4.49,4.5,4.75,4.66 +05/02/2024,5.51,5.47,5.46,5.5,5.42,5.16,4.87,4.71,4.57,4.57,4.58,4.82,4.72 +05/01/2024,5.47,5.5,5.46,5.51,5.43,5.21,4.96,4.79,4.64,4.64,4.63,4.85,4.74 +04/30/2024,5.48,5.51,5.46,5.45,5.44,5.25,5.04,4.87,4.72,4.71,4.69,4.9,4.79 +04/29/2024,5.48,5.51,5.45,5.45,5.43,5.2,4.97,4.8,4.65,4.64,4.63,4.86,4.75 +04/26/2024,5.48,5.51,5.46,5.45,5.4,5.21,4.96,4.84,4.68,4.68,4.67,4.89,4.78 +04/25/2024,5.48,5.51,5.47,5.46,5.41,5.21,4.96,4.85,4.7,4.71,4.7,4.93,4.82 +04/24/2024,5.49,5.5,5.46,5.44,5.4,5.17,4.89,4.78,4.64,4.66,4.65,4.88,4.78 +04/23/2024,5.49,5.5,5.45,5.44,5.39,5.14,4.86,4.76,4.63,4.62,4.61,4.84,4.73 +04/22/2024,5.49,5.5,5.42,5.44,5.39,5.16,4.97,4.81,4.66,4.65,4.62,4.84,4.72 +04/19/2024,5.49,5.51,5.45,5.44,5.39,5.17,4.97,4.81,4.66,4.65,4.62,4.83,4.72 +04/18/2024,5.49,5.52,5.46,5.44,5.39,5.18,4.98,4.83,4.68,4.67,4.64,4.85,4.74 +04/17/2024,5.49,5.49,5.45,5.44,5.38,5.16,4.93,4.77,4.62,4.61,4.59,4.81,4.71 +04/16/2024,5.49,5.51,5.45,5.44,5.39,5.18,4.97,4.83,4.69,4.69,4.67,4.88,4.77 +04/15/2024,5.49,5.51,5.45,5.44,5.38,5.16,4.93,4.78,4.65,4.65,4.63,4.85,4.74 +04/12/2024,5.48,5.5,5.45,5.42,5.36,5.13,4.88,4.7,4.54,4.53,4.5,4.73,4.61 +04/11/2024,5.48,5.51,5.45,5.44,5.38,5.17,4.93,4.77,4.61,4.6,4.56,4.77,4.65 +04/10/2024,5.49,5.5,5.45,5.44,5.4,5.19,4.97,4.77,4.61,4.59,4.55,4.76,4.64 +04/09/2024,5.48,5.49,5.43,5.41,5.34,5.03,4.74,4.52,4.37,4.38,4.36,4.6,4.5 +04/08/2024,5.48,5.49,5.43,5.41,5.35,5.07,4.78,4.6,4.43,4.43,4.42,4.65,4.55 +04/05/2024,5.47,5.5,5.43,5.41,5.34,5.05,4.73,4.54,4.38,4.39,4.39,4.65,4.54 +04/04/2024,5.47,5.49,5.41,5.4,5.32,5.0,4.65,4.46,4.3,4.31,4.31,4.57,4.47 +04/03/2024,5.47,5.44,5.42,5.4,5.33,5.03,4.68,4.48,4.34,4.36,4.36,4.61,4.51 +04/02/2024,5.49,5.45,5.42,5.4,5.34,5.05,4.7,4.51,4.35,4.37,4.36,4.61,4.51 +04/01/2024,5.49,5.47,5.44,5.41,5.36,5.06,4.72,4.51,4.34,4.33,4.33,4.58,4.47 +03/28/2024,5.49,5.48,5.46,5.42,5.38,5.03,4.59,4.4,4.21,4.2,4.2,4.45,4.34 +03/27/2024,5.5,5.47,5.45,5.41,5.36,4.99,4.54,4.36,4.18,4.18,4.2,4.45,4.36 +03/26/2024,5.5,5.47,5.46,5.41,5.36,5.0,4.56,4.38,4.22,4.23,4.24,4.49,4.4 +03/25/2024,5.51,5.48,5.46,5.41,5.36,5.0,4.54,4.39,4.23,4.25,4.25,4.51,4.42 +03/22/2024,5.51,5.47,5.46,5.4,5.34,4.98,4.59,4.36,4.2,4.22,4.22,4.47,4.39 +03/21/2024,5.51,5.48,5.48,5.4,5.36,5.01,4.62,4.42,4.26,4.28,4.27,4.53,4.44 +03/20/2024,5.5,5.47,5.47,5.41,5.36,5.01,4.59,4.41,4.25,4.28,4.27,4.53,4.45 +03/19/2024,5.52,5.48,5.48,5.41,5.39,5.06,4.68,4.47,4.31,4.31,4.3,4.54,4.44 +03/18/2024,5.52,5.48,5.48,5.41,5.39,5.06,4.73,4.52,4.36,4.35,4.34,4.57,4.46 +03/15/2024,5.52,5.48,5.48,5.41,5.38,5.05,4.72,4.51,4.33,4.33,4.31,4.55,4.43 +03/14/2024,5.52,5.48,5.48,5.42,5.38,5.04,4.68,4.46,4.29,4.3,4.29,4.54,4.44 +03/13/2024,5.52,5.47,5.48,5.41,5.37,5.01,4.61,4.37,4.19,4.2,4.19,4.45,4.35 +03/12/2024,5.52,5.48,5.48,5.41,5.37,5.0,4.58,4.33,4.15,4.16,4.16,4.42,4.31 +03/11/2024,5.5,5.47,5.48,5.41,5.35,4.95,4.51,4.26,4.08,4.09,4.1,4.36,4.26 +03/08/2024,5.51,5.48,5.46,5.4,5.34,4.92,4.48,4.25,4.06,4.08,4.09,4.36,4.26 +03/07/2024,5.51,5.48,5.47,5.4,5.34,4.93,4.5,4.28,4.07,4.09,4.09,4.35,4.25 +03/06/2024,5.5,5.47,5.47,5.4,5.35,4.95,4.55,4.32,4.12,4.12,4.11,4.36,4.24 +03/05/2024,5.5,5.47,5.47,5.4,5.35,4.94,4.54,4.32,4.13,4.15,4.13,4.39,4.27 +03/04/2024,5.51,5.49,5.48,5.42,5.37,4.98,4.61,4.39,4.21,4.23,4.22,4.48,4.36 +03/01/2024,5.54,5.49,5.42,5.41,5.27,4.94,4.54,4.32,4.17,4.2,4.19,4.46,4.33 +02/29/2024,5.53,5.5,5.45,5.43,5.3,5.01,4.64,4.43,4.26,4.28,4.25,4.51,4.38 +02/28/2024,5.5,5.51,5.45,5.43,5.31,5.0,4.64,4.44,4.26,4.28,4.27,4.53,4.4 +02/27/2024,5.5,5.52,5.45,5.47,5.33,5.03,4.7,4.5,4.32,4.34,4.31,4.57,4.44 +02/26/2024,5.5,5.52,5.47,5.47,5.34,5.03,4.69,4.48,4.29,4.32,4.28,4.53,4.4 +02/23/2024,5.49,5.51,5.46,5.46,5.32,5.0,4.67,4.45,4.28,4.28,4.26,4.51,4.37 +02/22/2024,5.49,5.51,5.45,5.45,5.32,5.02,4.69,4.49,4.33,4.35,4.33,4.58,4.47 +02/21/2024,5.5,5.5,5.44,5.45,5.32,4.98,4.64,4.43,4.3,4.33,4.32,4.59,4.49 +02/20/2024,5.49,5.49,5.44,5.45,5.32,4.97,4.59,4.38,4.25,4.28,4.27,4.56,4.44 +02/16/2024,5.48,5.51,5.44,5.45,5.31,4.98,4.64,4.43,4.29,4.31,4.3,4.58,4.45 +02/15/2024,5.49,5.51,5.43,5.45,5.3,4.93,4.56,4.36,4.22,4.25,4.24,4.54,4.42 +02/14/2024,5.48,5.51,5.43,5.45,5.31,4.94,4.56,4.38,4.25,4.27,4.27,4.57,4.45 +02/13/2024,5.48,5.52,5.45,5.46,5.32,4.99,4.64,4.44,4.31,4.33,4.31,4.59,4.46 +02/12/2024,5.49,5.51,5.43,5.43,5.27,4.87,4.46,4.25,4.13,4.16,4.17,4.48,4.37 +02/09/2024,5.49,5.51,5.44,5.43,5.26,4.86,4.48,4.25,4.14,4.17,4.17,4.48,4.37 +02/08/2024,5.49,5.51,5.44,5.42,5.24,4.83,4.46,4.22,4.12,4.15,4.15,4.47,4.36 +02/07/2024,5.47,5.49,5.43,5.4,5.23,4.83,4.41,4.16,4.06,4.09,4.09,4.41,4.31 +02/06/2024,5.48,5.5,5.44,5.41,5.23,4.82,4.39,4.14,4.03,4.07,4.09,4.39,4.29 +02/05/2024,5.49,5.5,5.42,5.42,5.25,4.87,4.46,4.27,4.13,4.16,4.17,4.46,4.35 +02/02/2024,5.49,5.51,5.43,5.42,5.22,4.81,4.36,4.14,3.99,4.02,4.03,4.33,4.22 +02/01/2024,5.49,5.51,5.42,5.38,5.15,4.68,4.2,3.96,3.8,3.83,3.87,4.21,4.1 +01/31/2024,5.53,5.46,5.42,5.4,5.18,4.73,4.27,4.05,3.91,3.95,3.99,4.34,4.22 +01/30/2024,5.53,5.47,5.42,5.38,5.19,4.8,4.36,4.14,4.0,4.03,4.06,4.4,4.28 +01/29/2024,5.53,5.46,5.42,5.37,5.19,4.76,4.29,4.1,3.97,4.02,4.08,4.42,4.31 +01/26/2024,5.54,5.45,5.44,5.39,5.19,4.78,4.34,4.15,4.04,4.1,4.15,4.49,4.38 +01/25/2024,5.54,5.48,5.44,5.39,5.19,4.76,4.28,4.12,4.01,4.07,4.14,4.49,4.38 +01/24/2024,5.52,5.44,5.44,5.4,5.22,4.83,4.34,4.19,4.06,4.14,4.18,4.52,4.41 +01/23/2024,5.53,5.46,5.45,5.38,5.21,4.81,4.31,4.16,4.06,4.11,4.14,4.48,4.38 +01/22/2024,5.53,5.47,5.46,5.39,5.22,4.83,4.37,4.14,4.03,4.07,4.11,4.44,4.32 +01/19/2024,5.54,5.47,5.45,5.39,5.21,4.84,4.39,4.18,4.08,4.12,4.15,4.47,4.36 +01/18/2024,5.53,5.48,5.45,5.39,5.2,4.8,4.34,4.13,4.04,4.1,4.14,4.48,4.37 +01/17/2024,5.54,5.47,5.47,5.4,5.2,4.8,4.34,4.12,4.02,4.07,4.1,4.42,4.31 +01/16/2024,5.54,5.47,5.45,5.37,5.18,4.7,4.22,4.02,3.95,4.01,4.07,4.43,4.3 +01/12/2024,5.55,5.47,5.45,5.37,5.16,4.65,4.14,3.92,3.84,3.91,3.96,4.32,4.2 +01/11/2024,5.54,5.47,5.46,5.38,5.22,4.75,4.26,4.02,3.9,3.95,3.98,4.32,4.18 +01/10/2024,5.53,5.46,5.46,5.39,5.23,4.82,4.37,4.1,3.99,4.01,4.04,4.35,4.2 +01/09/2024,5.53,5.46,5.47,5.38,5.24,4.82,4.36,4.09,3.97,4.0,4.02,4.33,4.18 +01/08/2024,5.54,5.48,5.49,5.39,5.24,4.82,4.36,4.11,3.97,3.99,4.01,4.33,4.17 +01/05/2024,5.54,5.48,5.47,5.41,5.24,4.84,4.4,4.17,4.02,4.04,4.05,4.37,4.21 +01/04/2024,5.56,5.48,5.48,5.41,5.25,4.85,4.38,4.14,3.97,3.99,3.99,4.3,4.13 +01/03/2024,5.54,5.54,5.48,5.41,5.25,4.81,4.33,4.07,3.9,3.92,3.91,4.21,4.05 +01/02/2024,5.55,5.54,5.46,5.41,5.24,4.8,4.33,4.09,3.93,3.95,3.95,4.25,4.08 diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000..76d0762 Binary files /dev/null and b/dump.rdb differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..8ada520 --- /dev/null +++ b/main.py @@ -0,0 +1,248 @@ +import io +import json +import os +import pickle +import threading +import time +import uuid +from enum import Enum + +import pandas as pd +import requests +from flask import Flask, jsonify, render_template, request, redirect, make_response + + +app = Flask(__name__) + +TREASURY_URL = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/daily-treasury-rates.csv/2024/all?type=daily_treasury_yield_curve&field_tdr_date_value=2024&page&_format=csv" + +CURRENT_DATE = 20240524 # Set to friday since market is closed + +RATES = None + + +class Side(str, Enum): + BUY = "BUY" + SELL = "SELL" + + +# Since we know all the types of treasuries, just enumerate them +class Instrument(str, Enum): + I_1MO = "1 Mo" + I_2MO = "2 Mo" + I_3MO = "3 Mo" + I_4MO = "4 Mo" + I_6MO = "6 Mo" + I_1YR = "1 Yr" + I_2YR = "2 Yr" + I_3YR = "3 Yr" + I_5YR = "5 Yr" + I_7YR = "7 Yr" + I_10YR = "10 Yr" + I_20YR = "12 Yr" + I_30YR = "30 Yr" + + @classmethod + def from_text(cls, txt): + if txt == "1 Mo": + return Instrument.I_1MO + elif txt == "2 Mo": + return Instrument.I_2MO + elif txt == "3 Mo": + return Instrument.I_3MO + elif txt == "4 Mo": + return Instrument.I_4MO + elif txt == "6 Mo": + return Instrument.I_6MO + elif txt == "1 Yr": + return Instrument.I_1YR + elif txt == "2 Yr": + return Instrument.I_2YR + elif txt == "3 Yr": + return Instrument.I_3YR + elif txt == "5 Yr": + return Instrument.I_5YR + elif txt == "7 Yr": + return Instrument.I_7YR + elif txt == "10 Yr": + return Instrument.I_20YR + elif txt == "12 Yr": + return Instrument.I_20YR + return Instrument.I_30YR + + +class Transaction: + def __init__( + self, user: str, instrument: Instrument, quantity: int, date: int, side: Side, rate: float + ): + self.user = user + self.instrument: str = str(instrument.value) + self.side: str = str(side.value) + self.quantity = quantity + self.date = date + self.rate = rate + + +class User: + def __init__(self, balance: int = 100000) -> None: + self.ID = str(uuid.uuid4()) + self.balance = balance + + +USERS: dict[str, User] = {} +TRANSACTIONS: dict[str, list[Transaction]] = {} + + +def background_saver(): + while True: + with open("users.p", "wb") as fp: + pickle.dump(USERS, fp) + with open("transactions.p", "wb") as fp: + pickle.dump(TRANSACTIONS, fp) + + time.sleep(5) + + +def get_treasury_rates_for_year(year: int) -> pd.DataFrame | None: + global RATES + + use_cache = False + if RATES is None: + if os.path.isfile("./cache.csv"): + if time.time() - os.path.getmtime("./cache.csv") < 60000: + use_cache = True + else: + use_cache = True + + if use_cache: + csv: pd.DataFrame = pd.read_csv("./cache.csv") + RATES = csv + else: + response = requests.get(TREASURY_URL) + response.raise_for_status() + raw_csv = response.content.decode("UTF-8") + csv_io = io.StringIO(raw_csv) + csv: pd.DataFrame = pd.read_csv(csv_io) + csv.to_csv("./cache.csv", index=False) + RATES = csv + + ret = {} + cols = list(RATES.columns.values) + for col in cols: + ret[col] = RATES[col].tolist() + + dates = ret["Date"] + del ret["Date"] + return ret, dates + + +@app.route("/api/transactions/") +def get_transactions_for_user(id): + t = TRANSACTIONS.get(id) + if t is None: + return jsonify([]) + + return jsonify([{"term": tx.instrument, "quantity": tx.quantity, "side": tx.side, "date": tx.date, "rate": tx.rate} for tx in t]) + + +@app.route("/api/buy", methods=["POST"]) +def buy(): + content = request.json + print(content) + q = int(content["quantity"]) + i = content["instrument"] + user_id = content["user_id"] + cost = q * 100 + if USERS.get(user_id) is not None: + if cost > USERS[user_id].balance: + return "You do not have the funds for this transaction", 400 + else: + return "User does not exist", 404 + + current_rate = RATES[i].tolist()[-1] + + USERS[user_id].balance = USERS[user_id].balance - cost + t = Transaction(user_id, Instrument.from_text(i), q, time.time(), Side.BUY, current_rate) + + if TRANSACTIONS.get(user_id) is None: + TRANSACTIONS[user_id] = [t] + else: + TRANSACTIONS[user_id].append(t) + + return jsonify(t) + + +@app.route("/api/login", methods=["POST"]) +def login(): + content = request.json + id = content.get("id") + if id is None: + return "ID cannot be empty", 400 + u = USERS.get(id) + if u is None: + return "User does not exist", 404 + resp = make_response(redirect("/")) + resp.set_cookie("userid", u.ID) + return resp + + +@app.route("/api/createuser", methods=["POST"]) +def createuser(): + u = User() + USERS[u.ID] = u + resp = make_response(redirect("/")) + resp.set_cookie("userid", u.ID) + return resp + + +@app.route("/register") +def register(): + if "userid" in request.cookies: + return redirect("/") + return render_template("register.html") + + +@app.route("/logout") +def logout(): + if "userid" not in request.cookies: + return redirect("/register") + resp = make_response(redirect("/register")) + resp.set_cookie("userid", "", expires=0) + return resp + + +@app.route("/") +def index(): + if "userid" not in request.cookies: + return redirect("/register") + uid = request.cookies.get("userid") + if uid not in USERS: + resp = make_response(redirect("/register")) + resp.set_cookie("userid", "", expires=0) + return resp + rates, dates = get_treasury_rates_for_year(2024) + return render_template("index.html", rates=[rates], dates=dates, userid=uid, balance=USERS[uid].balance) + + +if __name__ == "__main__": + # Load up files if they exist + if os.path.isfile("./users.p"): + print("Loading users") + with open("./users.p", "rb") as fp: + USERS = pickle.load(fp) + + if os.path.isfile("./transactions.p"): + print("Loading transactions") + with open("./transactions.p", "rb") as fp: + TRANSACTIONS = pickle.load(fp) + + # Start persistence + thread = threading.Thread(target=background_saver) + thread.start() + + # Start Server + app.run(port=8008) + + +thread = threading.Thread(target=background_saver) +thread.start() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..9f17506 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,50 @@ + + + + + + + DWSFi + + + + + + + + +
+ +
+
+
+ {% block content %}{% endblock %} +
+
+
+
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..fd5e896 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,220 @@ +{% extends 'base.html' %} + + +{% block content %} +
+
+
+
+
+
Account Balance
+
+
+
+
+
${{balance}}
+
+
+
+
+
+
+
+
+
+
Treasury Yields
+
+
+ +
+
+
+
+ +
+
+
+

Transactions

+
+
+ + + + + + + + + + + + +
TermQuantityDateSideRate
+
+
+
+ +
+
+
+

Buy/Sell

+
+
+
+
Term
+ +
+
+ + +
+
+ + +
+ + +
+
+
+ +
+ + + + + +{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..6a489b4 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,64 @@ + +{% extends 'base.html' %} + + +{% block content %} +
+

Registration

+
+
+
+

Registration will assign you a UUID, please remember this UUID to login!

+ + +
+
+
+
+
+
+

If you already have a UUID, enter it below to login

+ +
+ + +
+ + +
+
+
+
+ + +{% endblock %} diff --git a/transactions.p b/transactions.p new file mode 100644 index 0000000..1bfa8ca Binary files /dev/null and b/transactions.p differ diff --git a/users.p b/users.p new file mode 100644 index 0000000..81c0fa2 Binary files /dev/null and b/users.p differ